Compare commits

..

40 Commits

Author SHA1 Message Date
RiotRobot 121250a6fb v20.1.0-rc.2 2022-10-05 13:23:42 +01:00
RiotRobot a7aa227f55 Prepare changelog for v20.1.0-rc.2 2022-10-05 13:23:42 +01:00
ElementRobot 04c1dfe43a Use the correct sender key when checking shared secret (#2730) (#2731)
(cherry picked from commit 890a840685)

Co-authored-by: Hubert Chathi <hubertc@matrix.org>
2022-10-04 11:27:40 -06:00
RiotRobot 07476a0ae0 v20.1.0-rc.1 2022-10-04 14:01:35 +01:00
RiotRobot 6348704bec Prepare changelog for v20.1.0-rc.1 2022-10-04 14:01:34 +01:00
mcalinghee f84a33910c Unexpected ignored self key request when it's not shared history (#2724)
* ignore forwarded key process also if the user is not the same
2022-10-04 14:31:21 +02:00
Germain 0ccf7c50f2 Add custom error message for restricted import (#2727) 2022-10-04 09:59:20 +01:00
Šimon Brandner ead33003b7 Check for the spec version when determining private read receipt support (#2722) 2022-10-04 06:50:42 +00:00
Faye Duxovni 7d5360a00f Rename redecryption-related function arguments for clarity (#2709) 2022-10-03 19:51:30 -04:00
renovate[bot] 4b283015ba Lock file maintenance (#2721)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-03 08:30:28 +01:00
Michael Telatynski 887e15aac5 Fix IDB initial migration handling causing spurious lazy loading upgrade loops (#2718) 2022-09-30 10:41:04 +00:00
RiotRobot a83d80f502 Merge branch 'master' into develop 2022-09-30 10:59:52 +01:00
Michael Telatynski 5afe373446 Fix release-npm.yml dist-tag npm token passing mechanism (#2717) 2022-09-30 10:56:36 +01:00
Stanislav Demydiuk 9bb5afe5c0 Fix issue in sync when crypto is not supported by client (#2715)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-09-30 08:12:31 +00:00
Michael Telatynski f349663329 Add CI to protect against mixing src and lib imports (#2704) 2022-09-30 09:05:28 +01:00
Faye Duxovni f398e3564d Calculate IndexedDB versions automatically to reduce repeated information and possibilities for error (#2713) 2022-09-29 16:25:19 -04:00
RiotRobot 91171afddd Merge branch 'master' into develop 2022-09-28 17:48:45 +01:00
Faye Duxovni f410e71bfa Fix missing return when receiving an invitation without shared history (#2710) 2022-09-28 17:36:09 +01:00
RiotRobot 83fca5b57d Merge branch 'master' into develop 2022-09-28 15:46:11 +01:00
Faye Duxovni 4ba083e6af Bump IDB crypto store version (#2705)
* Bump IDB crypto store version

* lint fix
2022-09-28 15:39:37 +01:00
Germain 0403e4bedc Fix incorrect MSC3890 unstable prefix (#2703) 2022-09-28 15:37:13 +02:00
RiotRobot 14aa7846a5 Merge branch 'master' into develop 2022-09-28 14:05:21 +01:00
RiotRobot 45348a354e Resetting package fields for development 2022-09-27 16:48:03 +01:00
RiotRobot fa3339fc84 Merge branch 'master' into develop 2022-09-27 16:48:00 +01:00
Germain b332c6c4b9 Account data can return undefined (#2701) 2022-09-27 12:47:05 +00:00
Germain 209a101be7 Add local notification settings capability (#2700) 2022-09-27 11:41:20 +01:00
Michael Telatynski efbf5479d1 Remove redundant rel_branch logic (#2698) 2022-09-26 10:05:58 +01:00
Hugh Nimmo-Smith dacef048be Typings for MSC3824 (#2691) 2022-09-23 16:23:50 -06:00
Hugh Nimmo-Smith caadc6f95b Implementation of MSC3882 login token request (#2687) 2022-09-22 16:36:37 +01:00
Hugh Nimmo-Smith 39bc7e2bb3 Refer to explicit Matrix spec versions instead of Latest (#2668) 2022-09-22 15:28:11 +00:00
Hugh Nimmo-Smith 040f012350 Typings for MSC2965 OIDC provider discovery (#2424) 2022-09-22 16:27:13 +01:00
Hugh Nimmo-Smith 8b2b677f92 Add return type for loginFlows() (#2669)
Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-09-22 16:25:01 +01:00
Germain 2b1fab928b Add unstable device_id field for MSC3881 (#2688) 2022-09-22 15:44:54 +02:00
Germain 516f52c5a4 Support to remotely toggle push notifications (#2686) 2022-09-22 08:37:54 +00:00
Michael Telatynski 8599a98b47 Fix backpagination at end logic being spec non-conforming (#2680) 2022-09-21 16:35:07 +01:00
renovate[bot] 7e24cb6cae Update jest monorepo to v29.0.3 (#2682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 09:25:43 +01:00
renovate[bot] 38aa8d18c0 Update all (#2684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 09:13:36 +01:00
Germain 2967ee6309 Read receipts for threads (#2635) 2022-09-21 07:50:44 +00:00
renovate[bot] 2e10b6065c Update typescript-eslint monorepo to v5.38.0 (#2685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 08:42:30 +02:00
renovate[bot] 69057ee035 Update babel monorepo to v7.19.1 (#2681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 08:25:16 +02:00
42 changed files with 1928 additions and 1126 deletions
+21 -1
View File
@@ -1,14 +1,22 @@
module.exports = {
plugins: [
"matrix-org",
"import",
],
extends: [
"plugin:matrix-org/babel",
"plugin:import/typescript",
],
env: {
browser: true,
node: true,
},
settings: {
"import/resolver": {
typescript: true,
node: true,
},
},
// NOTE: These rules are frozen and new rules should not be added here.
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
rules: {
@@ -35,7 +43,19 @@ module.exports = {
"no-console": "error",
// restrict EventEmitters to force callers to use TypedEventEmitter
"no-restricted-imports": ["error", "events"],
"no-restricted-imports": ["error", {
name: "events",
message: "Please use TypedEventEmitter instead"
}],
"import/no-restricted-paths": ["error", {
"zones": [{
"target": "./src/",
"from": "./src/index.ts",
"message": "The package index is dynamic between src and lib depending on " +
"whether release or development, target the specific module or matrix.ts instead",
}],
}],
},
overrides: [{
files: [
+2 -1
View File
@@ -36,5 +36,6 @@ jobs:
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.version }}
+10
View File
@@ -23,6 +23,16 @@ jobs:
- name: Typecheck
run: "yarn run lint:types"
- name: Switch js-sdk to release mode
run: |
scripts/switch_package_to_release.js
yarn install
yarn run build:compile
yarn run build:types
- name: Typecheck (release mode)
run: "yarn run lint:types"
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
+21
View File
@@ -1,3 +1,24 @@
Changes in [20.1.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0-rc.2) (2022-10-05)
============================================================================================================
## 🐛 Bug Fixes
* Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374.
Changes in [20.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0-rc.1) (2022-10-04)
============================================================================================================
## ✨ Features
* Add local notification settings capability ([\#2700](https://github.com/matrix-org/matrix-js-sdk/pull/2700)).
* Implementation of MSC3882 login token request ([\#2687](https://github.com/matrix-org/matrix-js-sdk/pull/2687)). Contributed by @hughns.
* Typings for MSC2965 OIDC provider discovery ([\#2424](https://github.com/matrix-org/matrix-js-sdk/pull/2424)). Contributed by @hughns.
* Support to remotely toggle push notifications ([\#2686](https://github.com/matrix-org/matrix-js-sdk/pull/2686)).
* Read receipts for threads ([\#2635](https://github.com/matrix-org/matrix-js-sdk/pull/2635)).
## 🐛 Bug Fixes
* Unexpected ignored self key request when it's not shared history ([\#2724](https://github.com/matrix-org/matrix-js-sdk/pull/2724)). Contributed by @mcalinghee.
* Fix IDB initial migration handling causing spurious lazy loading upgrade loops ([\#2718](https://github.com/matrix-org/matrix-js-sdk/pull/2718)). Fixes vector-im/element-web#23377.
* Fix backpagination at end logic being spec non-conforming ([\#2680](https://github.com/matrix-org/matrix-js-sdk/pull/2680)). Fixes vector-im/element-web#22784.
Changes in [20.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.2) (2022-09-30)
==================================================================================================
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "20.0.2",
"version": "20.1.0-rc.2",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=12.9.0"
@@ -92,9 +92,10 @@
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "8.23.0",
"eslint": "8.23.1",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-matrix-org": "^0.6.0",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
+1 -13
View File
@@ -135,7 +135,6 @@ yarn install --ignore-scripts --pure-lockfile
# ignore leading v on release
release="${1#v}"
tag="v${release}"
rel_branch="release-$tag"
prerelease=0
# We check if this build is a prerelease by looking to
@@ -150,18 +149,7 @@ else
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
fi
# We might already be on the release branch, in which case, yay
# If we're on any branch starting with 'release', or the staging branch
# we don't create a separate release branch (this allows us to use the same
# release branch for releases and release candidates).
curbranch=$(git symbolic-ref --short HEAD)
if [[ "$curbranch" != release* && "$curbranch" != "staging" ]]; then
echo "Creating release branch"
git checkout -b "$rel_branch"
else
echo "Using current branch ($curbranch) for release"
rel_branch=$curbranch
fi
rel_branch=$(git symbolic-ref --short HEAD)
if [ -z "$skip_changelog" ]; then
echo "Generating changelog"
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env node
const fsProm = require('fs/promises');
const PKGJSON = 'package.json';
async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8'));
for (const field of ['main', 'typings']) {
if (pkgJson["matrix_lib_"+field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_"+field];
}
}
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
}
main();
@@ -833,6 +833,45 @@ describe("MatrixClient event timelines", function() {
]);
});
it("should stop paginating when it encounters no `end` token", () => {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, () => ({
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
}));
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
const params = req.queryParams;
expect(params.dir).toEqual("b");
expect(params.from).toEqual("start_token0");
expect(params.limit).toEqual("30");
}).respond(200, () => ({
start: "start_token0",
chunk: [],
}));
return Promise.all([
(async () => {
const tl = await client.getEventTimeline(timelineSet, EVENTS[0].event_id);
const success = await client.paginateEventTimeline(tl, { backwards: true });
expect(success).toBeFalsy();
expect(tl.getEvents().length).toEqual(1);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS)).toEqual(null);
expect(tl.getPaginationToken(EventTimeline.FORWARDS)).toEqual("end_token0");
})(),
httpBackend.flushAllExpected(),
]);
});
it("should allow you to paginate forwards", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
+24
View File
@@ -889,6 +889,7 @@ describe("MatrixClient", function() {
};
const prom = client.getPushers();
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {});
httpBackend.when("GET", "/pushers").respond(200, response);
await httpBackend.flush();
expect(await prom).toStrictEqual(response);
@@ -1074,6 +1075,29 @@ describe("MatrixClient", function() {
expect(await prom).toStrictEqual(response);
});
});
describe("requestLoginToken", () => {
it("should hit the expected API endpoint with UIA", async () => {
const response = {};
const uiaData = { foo: "baa" };
const prom = client.requestLoginToken(uiaData);
httpBackend
.when("POST", "/unstable/org.matrix.msc3882/login/token", { auth: uiaData })
.respond(200, response);
await httpBackend.flush();
expect(await prom).toStrictEqual(response);
});
it("should hit the expected API endpoint without UIA", async () => {
const response = {};
const prom = client.requestLoginToken();
httpBackend
.when("POST", "/unstable/org.matrix.msc3882/login/token", {})
.respond(200, response);
await httpBackend.flush();
expect(await prom).toStrictEqual(response);
});
});
});
function withThreadId(event, newThreadId) {
+12 -1
View File
@@ -6,7 +6,7 @@ import '../olm-loader';
import { logger } from '../../src/logger';
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, MatrixClient, MsgType } from "../../src";
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper";
@@ -371,3 +371,14 @@ export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent>
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
app_display_name: "app",
app_id: "123",
data: {},
device_display_name: "name",
kind: "http",
lang: "en",
pushkey: "pushpush",
...extra,
});
+43
View File
@@ -0,0 +1,43 @@
/*
Copyright 2022 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 { LocalNotificationSettings } from "../../src/@types/local_notifications";
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixClient } from "../../src/matrix";
import { TestClient } from '../TestClient';
let client: MatrixClient;
describe("Local notification settings", () => {
beforeEach(() => {
client = (new TestClient(
"@alice:matrix.org", "123", undefined, undefined, undefined,
)).client;
client.setAccountData = jest.fn();
});
describe("Lets you set local notification settings", () => {
it("stores settings in account data", () => {
const deviceId = "device";
const settings: LocalNotificationSettings = { is_silenced: true };
client.setLocalNotificationSettings(deviceId, settings);
expect(client.setAccountData).toHaveBeenCalledWith(
`${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`,
settings,
);
});
});
});
+1 -1
View File
@@ -565,7 +565,7 @@ describe("MSC3089TreeSpace", () => {
rooms = {};
rooms[tree.roomId] = parentRoom;
(<any>tree).room = parentRoom; // override readonly
client.getRoom = (r) => rooms[r];
client.getRoom = (r) => rooms[r ?? ""];
clientSendStateFn = jest.fn()
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
+69
View File
@@ -0,0 +1,69 @@
/*
Copyright 2022 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 MockHttpBackend from 'matrix-mock-request';
import { IHttpOpts, MatrixClient, PUSHER_ENABLED } from "../../src/matrix";
import { mkPusher } from '../test-utils/test-utils';
const realSetTimeout = setTimeout;
function flushPromises() {
return new Promise(r => {
realSetTimeout(r, 1);
});
}
let client: MatrixClient;
let httpBackend: MockHttpBackend;
describe("Pushers", () => {
beforeEach(() => {
httpBackend = new MockHttpBackend();
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: httpBackend.requestFn as unknown as IHttpOpts["request"],
});
});
describe("supports remotely toggling push notifications", () => {
it("migration support when connecting to a legacy homeserver", async () => {
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
unstable_features: {
"org.matrix.msc3881": false,
},
});
httpBackend.when("GET", "/pushers").respond(200, {
pushers: [
mkPusher(),
mkPusher({ [PUSHER_ENABLED.name]: true }),
mkPusher({ [PUSHER_ENABLED.name]: false }),
],
});
const promise = client.getPushers();
await httpBackend.flushAllExpected();
await flushPromises();
const response = await promise;
expect(response.pushers[0][PUSHER_ENABLED.name]).toBe(true);
expect(response.pushers[1][PUSHER_ENABLED.name]).toBe(true);
expect(response.pushers[2][PUSHER_ENABLED.name]).toBe(false);
});
});
});
+150
View File
@@ -0,0 +1,150 @@
/*
Copyright 2022 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 MockHttpBackend from 'matrix-mock-request';
import { ReceiptType } from '../../src/@types/read_receipts';
import { MatrixClient } from "../../src/client";
import { IHttpOpts } from '../../src/http-api';
import { EventType } from '../../src/matrix';
import { MAIN_ROOM_TIMELINE } from '../../src/models/read-receipt';
import { encodeUri } from '../../src/utils';
import * as utils from "../test-utils/test-utils";
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
// other async methods which break the event loop, letting scheduled promise
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
// it manually (this is what sinon does under the hood). We do both in a loop
// until the thing we expect happens: hopefully this is the least flakey way
// and avoids assuming anything about the app's behaviour.
const realSetTimeout = setTimeout;
function flushPromises() {
return new Promise(r => {
realSetTimeout(r, 1);
});
}
let client: MatrixClient;
let httpBackend: MockHttpBackend;
const THREAD_ID = "$thread_event_id";
const ROOM_ID = "!123:matrix.org";
const threadEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
"body": "Hello from a thread",
"m.relates_to": {
"event_id": THREAD_ID,
"m.in_reply_to": {
"event_id": THREAD_ID,
},
"rel_type": "m.thread",
},
},
});
const roomEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
"body": "Hello from a room",
},
});
function mockServerSideSupport(client, hasServerSideSupport) {
const doesServerSupportUnstableFeature = client.doesServerSupportUnstableFeature;
client.doesServerSupportUnstableFeature = (unstableFeature) => {
if (unstableFeature === "org.matrix.msc3771") {
return Promise.resolve(hasServerSideSupport);
} else {
return doesServerSupportUnstableFeature(unstableFeature);
}
};
}
describe("Read receipt", () => {
beforeEach(() => {
httpBackend = new MockHttpBackend();
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: httpBackend.requestFn as unknown as IHttpOpts["request"],
});
client.isGuest = () => false;
});
describe("sendReceipt", () => {
it("sends a thread read receipt", async () => {
httpBackend.when(
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: threadEvent.getId(),
}),
).check((request) => {
expect(request.data.thread_id).toEqual(THREAD_ID);
}).respond(200, {});
mockServerSideSupport(client, true);
client.sendReceipt(threadEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
it("sends a room read receipt", async () => {
httpBackend.when(
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: roomEvent.getId(),
}),
).check((request) => {
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
}).respond(200, {});
mockServerSideSupport(client, true);
client.sendReceipt(roomEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
it("sends a room read receipt when there's no server support", async () => {
httpBackend.when(
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: threadEvent.getId(),
}),
).check((request) => {
expect(request.data.thread_id).toBeUndefined();
}).respond(200, {});
mockServerSideSupport(client, false);
client.sendReceipt(threadEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
});
});
+30 -16
View File
@@ -32,13 +32,14 @@ import {
RoomEvent,
} from "../../src";
import { EventTimeline } from "../../src/models/event-timeline";
import { IWrappedReceipt, Room } from "../../src/models/room";
import { Room } from "../../src/models/room";
import { RoomState } from "../../src/models/room-state";
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
import { TestClient } from "../TestClient";
import { emitPromise } from "../test-utils/test-utils";
import { ReceiptType } from "../../src/@types/read_receipts";
import { Thread, ThreadEvent } from "../../src/models/thread";
import { WrappedReceipt } from "../../src/models/read-receipt";
describe("Room", function() {
const roomId = "!foo:bar";
@@ -1430,6 +1431,19 @@ describe("Room", function() {
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
});
});
describe("hasUserReadUpTo", function() {
it("should acknowledge if an event has been read", function() {
const ts = 13787898424;
room.addReceipt(mkReceipt(roomId, [
mkRecord(eventToAck.getId(), "m.read", userB, ts),
]));
expect(room.hasUserReadEvent(userB, eventToAck.getId())).toEqual(true);
});
it("return false for an unknown event", function() {
expect(room.hasUserReadEvent(userB, "unknown_event")).toEqual(false);
});
});
});
describe("tags", function() {
@@ -2439,8 +2453,8 @@ describe("Room", function() {
const room = new Room(roomId, client, userA);
it("handles missing receipt type", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as IWrappedReceipt : null;
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as WrappedReceipt : null;
};
expect(room.getEventReadUpTo(userA)).toEqual("eventId");
@@ -2448,12 +2462,12 @@ describe("Room", function() {
describe("prefers newer receipt", () => {
it("should compare correctly using timelines", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1" } as IWrappedReceipt;
return { eventId: "eventId1" } as WrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2" } as IWrappedReceipt;
return { eventId: "eventId2" } as WrappedReceipt;
}
return null;
};
@@ -2473,12 +2487,12 @@ describe("Room", function() {
room.getUnfilteredTimelineSet = () => ({
compareEventOrdering: (_1, _2) => null,
} as EventTimelineSet);
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as IWrappedReceipt;
return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as WrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2", data: { ts: i === 2 ? 2 : 1 } } as IWrappedReceipt;
return { eventId: "eventId2", data: { ts: i === 2 ? 2 : 1 } } as WrappedReceipt;
}
return null;
};
@@ -2491,9 +2505,9 @@ describe("Room", function() {
room.getUnfilteredTimelineSet = () => ({
compareEventOrdering: (_1, _2) => null,
} as EventTimelineSet);
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2", data: { ts: 1 } } as IWrappedReceipt;
return { eventId: "eventId2", data: { ts: 1 } } as WrappedReceipt;
}
return null;
};
@@ -2510,12 +2524,12 @@ describe("Room", function() {
});
it("should give precedence to m.read.private", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1" } as IWrappedReceipt;
return { eventId: "eventId1" } as WrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2" } as IWrappedReceipt;
return { eventId: "eventId2" } as WrappedReceipt;
}
return null;
};
@@ -2524,9 +2538,9 @@ describe("Room", function() {
});
it("should give precedence to m.read", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId3" } as IWrappedReceipt;
return { eventId: "eventId3" } as WrappedReceipt;
}
return null;
};
+54
View File
@@ -20,6 +20,7 @@ import 'jest-localstorage-mock';
import { IndexedDBStore, IStateEventWithRoomId, MemoryStore } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
import { defer } from "../../../src/utils";
describe("IndexedDBStore", () => {
afterEach(() => {
@@ -111,4 +112,57 @@ describe("IndexedDBStore", () => {
await store.setPendingEvents(roomId, []);
expect(localStorage.getItem("mx_pending_events_" + roomId)).toBeNull();
});
it("should resolve isNewlyCreated to true if no database existed initially", async () => {
const store = new IndexedDBStore({
indexedDB,
dbName: "db1",
localStorage,
});
await store.startup();
await expect(store.isNewlyCreated()).resolves.toBeTruthy();
});
it("should resolve isNewlyCreated to false if database existed already", async () => {
let store = new IndexedDBStore({
indexedDB,
dbName: "db2",
localStorage,
});
await store.startup();
store = new IndexedDBStore({
indexedDB,
dbName: "db2",
localStorage,
});
await store.startup();
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
});
it("should resolve isNewlyCreated to false if database existed already but needs upgrade", async () => {
const deferred = defer<Event>();
// seed db3 to Version 1 so it forces a migration
const req = indexedDB.open("matrix-js-sdk:db3", 1);
req.onupgradeneeded = () => {
const db = req.result;
db.createObjectStore("users", { keyPath: ["userId"] });
db.createObjectStore("accountData", { keyPath: ["type"] });
db.createObjectStore("sync", { keyPath: ["clobber"] });
};
req.onsuccess = deferred.resolve;
await deferred.promise;
req.result.close();
const store = new IndexedDBStore({
indexedDB,
dbName: "db3",
localStorage,
});
await store.startup();
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
});
});
+5 -1
View File
@@ -156,9 +156,13 @@ export interface IPusher {
lang: string;
profile_tag?: string;
pushkey: string;
enabled?: boolean | null | undefined;
"org.matrix.msc3881.enabled"?: boolean | null | undefined;
device_id?: string | null;
"org.matrix.msc3881.device_id"?: string | null;
}
export interface IPusherRequest extends IPusher {
export interface IPusherRequest extends Omit<IPusher, "device_id" | "org.matrix.msc3881.device_id"> {
append?: boolean;
}
+28 -3
View File
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { UnstableValue } from "../NamespacedValue";
// disable lint because these are wire responses
/* eslint-disable camelcase */
@@ -29,7 +31,7 @@ export interface IRefreshTokenResponse {
/* eslint-enable camelcase */
/**
* Response to GET login flows as per https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3login
* Response to GET login flows as per https://spec.matrix.org/v1.3/client-server-api/#get_matrixclientv3login
*/
export interface ILoginFlowsResponse {
flows: LoginFlow[];
@@ -45,13 +47,20 @@ export interface IPasswordFlow extends ILoginFlow {
type: "m.login.password";
}
export const DELEGATED_OIDC_COMPATIBILITY = new UnstableValue(
"delegated_oidc_compatibility",
"org.matrix.msc3824.delegated_oidc_compatibility",
);
/**
* Representation of SSO flow as per https://spec.matrix.org/latest/client-server-api/#client-login-via-sso
* Representation of SSO flow as per https://spec.matrix.org/v1.3/client-server-api/#client-login-via-sso
*/
export interface ISSOFlow extends ILoginFlow {
type: "m.login.sso" | "m.login.cas";
// eslint-disable-next-line camelcase
identity_providers?: IIdentityProvider[];
[DELEGATED_OIDC_COMPATIBILITY.name]?: boolean;
[DELEGATED_OIDC_COMPATIBILITY.altName]?: boolean;
}
export enum IdentityProviderBrand {
@@ -71,7 +80,7 @@ export interface IIdentityProvider {
}
/**
* Parameters to login request as per https://spec.matrix.org/latest/client-server-api/#login
* Parameters to login request as per https://spec.matrix.org/v1.3/client-server-api/#login
*/
/* eslint-disable camelcase */
export interface ILoginParams {
@@ -90,3 +99,19 @@ export enum SSOAction {
/** The user intends to register for a new account */
REGISTER = "register",
}
/**
* The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
* `m.login.token` issuance request.
* Note that this is UNSTABLE and subject to breaking changes without notice.
*/
export interface LoginTokenPostResponse {
/**
* The token to use with `m.login.token` to authenticate.
*/
login_token: string;
/**
* Expiration in seconds.
*/
expires_in: number;
}
+27
View File
@@ -191,6 +191,33 @@ export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
"m.visibility",
"org.matrix.msc3531.visibility");
/**
* https://github.com/matrix-org/matrix-doc/pull/3881
*
* @experimental
*/
export const PUSHER_ENABLED = new UnstableValue(
"enabled",
"org.matrix.msc3881.enabled");
/**
* https://github.com/matrix-org/matrix-doc/pull/3881
*
* @experimental
*/
export const PUSHER_DEVICE_ID = new UnstableValue(
"device_id",
"org.matrix.msc3881.device_id");
/**
* https://github.com/matrix-org/matrix-doc/pull/3890
*
* @experimental
*/
export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
"m.local_notification_settings",
"org.matrix.msc3890.local_notification_settings");
export interface IEncryptedFile {
url: string;
mimetype?: string;
+19
View File
@@ -0,0 +1,19 @@
/*
Copyright 2022 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 interface LocalNotificationSettings {
is_silenced: boolean;
}
+29
View File
@@ -0,0 +1,29 @@
/*
Copyright 2022 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 { IAuthData } from "../interactive-auth";
/**
* Helper type to represent HTTP request body for a UIA enabled endpoint
*/
export type UIARequest<T> = T & {
auth?: IAuthData;
};
/**
* Helper type to represent HTTP response body for a UIA enabled endpoint
*/
export type UIAResponse<T> = T | IAuthData;
+118 -24
View File
@@ -158,7 +158,9 @@ import {
} from "./@types/requests";
import {
EventType,
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
MsgType,
PUSHER_ENABLED,
RelationType,
RoomCreateTypeField,
RoomType,
@@ -188,7 +190,7 @@ import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, Rule
import { IThreepid } from "./@types/threepids";
import { CryptoStore } from "./crypto/store/base";
import { MediaHandler } from "./webrtc/mediaHandler";
import { IRefreshTokenResponse, SSOAction } from "./@types/auth";
import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter";
import { ReceiptType } from "./@types/read_receipts";
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
@@ -198,7 +200,10 @@ import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
import { UnstableValue } from "./NamespacedValue";
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
import { ToDeviceBatch } from "./models/ToDeviceMessage";
import { MAIN_ROOM_TIMELINE } from "./models/read-receipt";
import { IgnoredInvites } from "./models/invites-ignorer";
import { UIARequest, UIAResponse } from "./@types/uia";
import { LocalNotificationSettings } from "./@types/local_notifications";
export type Store = IStore;
@@ -523,10 +528,16 @@ interface IServerVersions {
unstable_features: Record<string, boolean>;
}
export const M_AUTHENTICATION = new UnstableValue(
"m.authentication",
"org.matrix.msc2965.authentication",
);
export interface IClientWellKnown {
[key: string]: any;
"m.homeserver"?: IWellKnownConfig;
"m.identity_server"?: IWellKnownConfig;
[M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965
}
export interface IWellKnownConfig {
@@ -538,6 +549,13 @@ export interface IWellKnownConfig {
base_url?: string | null;
}
export interface IDelegatedAuthConfig { // MSC2965
/** The OIDC Provider/issuer the client should use */
issuer: string;
/** The optional URL of the web UI where the user can manage their account */
account?: string;
}
interface IKeyBackupPath {
path: string;
queryData?: {
@@ -3332,7 +3350,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} roomId The room ID
* @return {Room|null} The Room or null if it doesn't exist or there is no data store.
*/
public getRoom(roomId: string): Room | null {
public getRoom(roomId: string | undefined): Room | null {
if (!roomId) {
return null;
}
return this.store.getRoom(roomId);
}
@@ -3421,7 +3442,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} eventType The event type
* @return {?object} The contents of the given account data event
*/
public getAccountData(eventType: string): MatrixEvent {
public getAccountData(eventType: string): MatrixEvent | undefined {
return this.store.getAccountData(eventType);
}
@@ -4590,7 +4611,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public sendReceipt(event: MatrixEvent, receiptType: ReceiptType, body: any, callback?: Callback): Promise<{}> {
public async sendReceipt(
event: MatrixEvent,
receiptType: ReceiptType,
body: any,
callback?: Callback,
): Promise<{}> {
if (typeof (body) === 'function') {
callback = body as any as Callback; // legacy
body = {};
@@ -4605,10 +4631,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$receiptType: receiptType,
$eventId: event.getId(),
});
// TODO: Add a check for which spec version this will be released in
if (await this.doesServerSupportUnstableFeature("org.matrix.msc3771")) {
const isThread = !!event.threadRootId;
body.thread_id = isThread
? event.threadRootId
: MAIN_ROOM_TIMELINE;
}
const promise = this.http.authedRequest(callback, Method.Post, path, undefined, body || {});
const room = this.getRoom(event.getRoomId());
if (room) {
if (room && this.credentials.userId) {
room.addLocalEchoReceipt(this.credentials.userId, event, receiptType);
}
return promise;
@@ -4622,7 +4657,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public async sendReadReceipt(event: MatrixEvent, receiptType = ReceiptType.Read, callback?: Callback): Promise<{}> {
public async sendReadReceipt(
event: MatrixEvent | null,
receiptType = ReceiptType.Read,
callback?: Callback,
): Promise<{} | undefined> {
if (!event) return;
const eventId = event.getId();
const room = this.getRoom(event.getRoomId());
if (room && room.hasPendingEvent(eventId)) {
@@ -5609,13 +5649,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.processBeaconEvents(timelineSet.room, timelineEvents);
this.processThreadEvents(room, threadedEvents, backwards);
const atEnd = res.end === undefined || res.end === res.start;
// if we've hit the end of the timeline, we need to stop trying to
// paginate. We need to keep the 'forwards' token though, to make sure
// we can recover from gappy syncs.
if (backwards && res.end == res.start) {
if (backwards && atEnd) {
eventTimeline.setPaginationToken(null, dir);
}
return res.end != res.start;
return !atEnd;
}).finally(() => {
eventTimeline.paginationRequests[dir] = null;
});
@@ -7081,10 +7123,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {Promise<ILoginFlowsResponse>} Resolves to the available login flows
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public loginFlows(callback?: Callback): Promise<any> { // TODO: Types
public loginFlows(callback?: Callback): Promise<ILoginFlowsResponse> {
return this.http.request(callback, Method.Get, "/login");
}
@@ -7253,6 +7295,27 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest(undefined, Method.Post, '/account/deactivate', undefined, body);
}
/**
* Make a request for an `m.login.token` to be issued as per
* [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882).
* The server may require User-Interactive auth.
* Note that this is UNSTABLE and subject to breaking changes without notice.
* @param {IAuthData} auth Optional. Auth data to supply for User-Interactive auth.
* @return {Promise<UIAResponse<LoginTokenPostResponse>>} Resolves: On success, the token response
* or UIA auth data.
*/
public requestLoginToken(auth?: IAuthData): Promise<UIAResponse<LoginTokenPostResponse>> {
const body: UIARequest<{}> = { auth };
return this.http.authedRequest(
undefined, // no callback support
Method.Post,
"/org.matrix.msc3882/login/token",
undefined, // no query params
body,
{ prefix: PREFIX_UNSTABLE },
);
}
/**
* Get the fallback URL to use for unknown interactive-auth stages.
*
@@ -7347,7 +7410,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$eventType: eventType,
});
return this.http.authedRequest(
undefined, Method.Get, path, null, null, {
undefined, Method.Get, path, undefined, undefined, {
prefix: PREFIX_UNSTABLE,
},
);
@@ -7544,7 +7607,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
[ReceiptType.Read]: rrEventId,
};
if (await this.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) {
if (
(await this.doesServerSupportUnstableFeature("org.matrix.msc2285.stable"))
|| (await this.isVersionSupported("v1.4"))
) {
content[ReceiptType.ReadPrivate] = rpEventId;
}
@@ -7652,7 +7718,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public getLocalAliases(roomId: string): Promise<{ aliases: string[] }> {
const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId });
const prefix = PREFIX_V3;
return this.http.authedRequest(undefined, Method.Get, path, null, null, { prefix });
return this.http.authedRequest(undefined, Method.Get, path, undefined, undefined, { prefix });
}
/**
@@ -7889,7 +7955,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
'bind': bind,
};
return this.http.authedRequest(
callback, Method.Post, path, null, data,
callback, Method.Post, path, undefined, data,
);
}
@@ -7908,7 +7974,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public async addThreePidOnly(data: IAddThreePidOnlyBody): Promise<{}> {
const path = "/account/3pid/add";
const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE;
return this.http.authedRequest(undefined, Method.Post, path, null, data, { prefix });
return this.http.authedRequest(undefined, Method.Post, path, undefined, data, { prefix });
}
/**
@@ -7930,7 +7996,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const prefix = await this.isVersionSupported("r0.6.0") ?
PREFIX_R0 : PREFIX_UNSTABLE;
return this.http.authedRequest(
undefined, Method.Post, path, null, data, { prefix },
undefined, Method.Post, path, undefined, data, { prefix },
);
}
@@ -7957,7 +8023,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
id_server: this.getIdentityServerUrl(true),
};
const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE;
return this.http.authedRequest(undefined, Method.Post, path, null, data, { prefix });
return this.http.authedRequest(undefined, Method.Post, path, undefined, data, { prefix });
}
/**
@@ -7974,7 +8040,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// eslint-disable-next-line camelcase
): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
const path = "/account/3pid/delete";
return this.http.authedRequest(undefined, Method.Post, path, null, { medium, address });
return this.http.authedRequest(undefined, Method.Post, path, undefined, { medium, address });
}
/**
@@ -8020,7 +8086,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
};
return this.http.authedRequest<{}>(
callback, Method.Post, path, null, data,
callback, Method.Post, path, undefined, data,
);
}
@@ -8111,8 +8177,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {Promise} Resolves: Array of objects representing pushers
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public getPushers(callback?: Callback): Promise<{ pushers: IPusher[] }> {
return this.http.authedRequest(callback, Method.Get, "/pushers");
public async getPushers(callback?: Callback): Promise<{ pushers: IPusher[] }> {
const response = await this.http.authedRequest(callback, Method.Get, "/pushers");
// Migration path for clients that connect to a homeserver that does not support
// MSC3881 yet, see https://github.com/matrix-org/matrix-spec-proposals/blob/kerry/remote-push-toggle/proposals/3881-remote-push-notification-toggling.md#migration
if (!await this.doesServerSupportUnstableFeature("org.matrix.msc3881")) {
response.pushers = response.pushers.map(pusher => {
if (!pusher.hasOwnProperty(PUSHER_ENABLED.name)) {
pusher[PUSHER_ENABLED.name] = true;
}
return pusher;
});
}
return response;
}
/**
@@ -8125,7 +8204,22 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/
public setPusher(pusher: IPusherRequest, callback?: Callback): Promise<{}> {
const path = "/pushers/set";
return this.http.authedRequest(callback, Method.Post, path, null, pusher);
return this.http.authedRequest(callback, Method.Post, path, undefined, pusher);
}
/**
* Persists local notification settings
* @param {string} deviceId
* @param {LocalNotificationSettings} notificationSettings
* @return {Promise} Resolves: an empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public setLocalNotificationSettings(
deviceId: string,
notificationSettings: LocalNotificationSettings,
): Promise<{}> {
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
return this.setAccountData(key, notificationSettings);
}
/**
@@ -8912,7 +9006,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$eventId: eventId,
});
return this.http.authedRequest(undefined, Method.Post, path, null, { score, reason });
return this.http.authedRequest(undefined, Method.Post, path, undefined, { score, reason });
}
/**
@@ -9074,7 +9168,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/
public async getRoomSummary(roomIdOrAlias: string, via?: string[]): Promise<IRoomSummary> {
const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias });
return this.http.authedRequest(undefined, Method.Get, path, { via }, null, {
return this.http.authedRequest(undefined, Method.Get, path, { via }, undefined, {
qsStringifyOptions: { arrayFormat: 'repeat' },
prefix: "/_matrix/client/unstable/im.nheko.summary",
});
+1 -1
View File
@@ -549,7 +549,7 @@ export class SecretStorage {
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM,
content.sender_key,
event.getSenderKey() || "",
);
if (senderKeyUser !== event.getSender()) {
logger.error("sending device does not belong to the user it claims to be from");
+1 -1
View File
@@ -23,7 +23,7 @@ limitations under the License.
import { MatrixClient } from "../../client";
import { Room } from "../../models/room";
import { OlmDevice } from "../OlmDevice";
import { MatrixEvent, RoomMember } from "../..";
import { MatrixEvent, RoomMember } from "../../matrix";
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
import { DeviceInfo } from "../deviceinfo";
import { IRoomEncryption } from "../RoomList";
+13 -7
View File
@@ -1444,9 +1444,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
memberEvent?.getPrevContent()?.membership === "invite");
const fromUs = event.getSender() === this.baseApis.getUserId();
if (!weRequested) {
// If someone sends us an unsolicited key and it's not
// shared history, ignore it
if (!weRequested && !fromUs) {
// If someone sends us an unsolicited key and they're
// not one of our other devices and it's not shared
// history, ignore it
if (!extraSessionData.sharedHistory) {
logger.log("forwarded key not shared history - ignoring");
return;
@@ -1455,7 +1456,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
// If someone sends us an unsolicited key for a room
// we're already in, and they're not one of our other
// devices or the one who invited us, ignore it
if (room && !fromInviter && !fromUs) {
if (room && !fromInviter) {
logger.log("forwarded key not from inviter or from us - ignoring");
return;
}
@@ -1795,12 +1796,17 @@ class MegolmDecryption extends DecryptionAlgorithm {
* @private
* @param {String} senderKey
* @param {String} sessionId
* @param {Boolean} keyTrusted
* @param {Boolean} forceRedecryptIfUntrusted whether messages that were already
* successfully decrypted using untrusted keys should be re-decrypted
*
* @return {Boolean} whether all messages were successfully
* decrypted with trusted keys
*/
private async retryDecryption(senderKey: string, sessionId: string, keyTrusted?: boolean): Promise<boolean> {
private async retryDecryption(
senderKey: string,
sessionId: string,
forceRedecryptIfUntrusted?: boolean,
): Promise<boolean> {
const senderPendingEvents = this.pendingEvents.get(senderKey);
if (!senderPendingEvents) {
return true;
@@ -1815,7 +1821,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
await Promise.all([...pending].map(async (ev) => {
try {
await ev.attemptDecryption(this.crypto, { isRetry: true, keyTrusted });
await ev.attemptDecryption(this.crypto, { isRetry: true, forceRedecryptIfUntrusted });
} catch (e) {
// don't die if something goes wrong
}
+1 -1
View File
@@ -30,7 +30,7 @@ import {
registerAlgorithm,
} from "./base";
import { Room } from '../../models/room';
import { MatrixEvent } from "../..";
import { MatrixEvent } from "../../models/event";
import { IEventDecryptionResult } from "../index";
import { IInboundSession } from "../OlmDevice";
@@ -34,7 +34,6 @@ import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
export const VERSION = 11;
const PROFILE_TRANSACTIONS = false;
/**
@@ -950,45 +949,34 @@ export class Backend implements CryptoStore {
}
}
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
logger.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
);
if (oldVersion < 1) { // The database did not previously exist.
createDatabase(db);
}
if (oldVersion < 2) {
db.createObjectStore("account");
}
if (oldVersion < 3) {
type DbMigration = (db: IDBDatabase) => void;
const DB_MIGRATIONS: DbMigration[] = [
(db) => { createDatabase(db); },
(db) => { db.createObjectStore("account"); },
(db) => {
const sessionsStore = db.createObjectStore("sessions", {
keyPath: ["deviceKey", "sessionId"],
});
sessionsStore.createIndex("deviceKey", "deviceKey");
}
if (oldVersion < 4) {
},
(db) => {
db.createObjectStore("inbound_group_sessions", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
if (oldVersion < 5) {
db.createObjectStore("device_data");
}
if (oldVersion < 6) {
db.createObjectStore("rooms");
}
if (oldVersion < 7) {
},
(db) => { db.createObjectStore("device_data"); },
(db) => { db.createObjectStore("rooms"); },
(db) => {
db.createObjectStore("sessions_needing_backup", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
if (oldVersion < 8) {
},
(db) => {
db.createObjectStore("inbound_group_sessions_withheld", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
if (oldVersion < 9) {
},
(db) => {
const problemsStore = db.createObjectStore("session_problems", {
keyPath: ["deviceKey", "time"],
});
@@ -997,18 +985,29 @@ export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
db.createObjectStore("notified_error_devices", {
keyPath: ["userId", "deviceId"],
});
}
if (oldVersion < 10) {
},
(db) => {
db.createObjectStore("shared_history_inbound_group_sessions", {
keyPath: ["roomId"],
});
}
if (oldVersion < 11) {
},
(db) => {
db.createObjectStore("parked_shared_history", {
keyPath: ["roomId"],
});
}
},
// Expand as needed.
];
export const VERSION = DB_MIGRATIONS.length;
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
logger.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
);
DB_MIGRATIONS.forEach((migration, index) => {
if (oldVersion <= index) migration(db);
});
}
function createDatabase(db: IDBDatabase): void {
+7 -7
View File
@@ -365,7 +365,7 @@ export class MatrixHttpApi {
// we're setting opts.json=false so that it doesn't JSON-encode the
// request, which also means it doesn't JSON-decode the response. Either
// way, we have to JSON-parse the response ourselves.
let bodyParser = null;
let bodyParser: ((body: string) => any) | undefined;
if (!rawResponse) {
bodyParser = function(rawBody: string) {
let body = JSON.parse(rawBody);
@@ -472,7 +472,7 @@ export class MatrixHttpApi {
headers["Content-Length"] = "0";
}
promise = this.authedRequest(
promise = this.authedRequest<UploadContentResponseType<O>>(
opts.callback, Method.Post, "/upload", queryParams, body, {
prefix: "/_matrix/media/r0",
headers,
@@ -590,10 +590,10 @@ export class MatrixHttpApi {
* occurred. This includes network problems and Matrix-specific error JSON.
*/
public authedRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>,
callback: Callback<T> | undefined,
method: Method,
path: string,
queryParams?: Record<string, string | string[]>,
queryParams?: Record<string, string | string[] | undefined>,
data?: CoreOptions["body"],
opts?: O | number, // number is legacy
): IAbortablePromise<ResponseType<T, O>> {
@@ -667,7 +667,7 @@ export class MatrixHttpApi {
* occurred. This includes network problems and Matrix-specific error JSON.
*/
public request<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>,
callback: Callback<T> | undefined,
method: Method,
path: string,
queryParams?: CoreOptions["qs"],
@@ -711,7 +711,7 @@ export class MatrixHttpApi {
* occurred. This includes network problems and Matrix-specific error JSON.
*/
public requestOtherUrl<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>,
callback: Callback<T> | undefined,
method: Method,
uri: string,
queryParams?: CoreOptions["qs"],
@@ -778,7 +778,7 @@ export class MatrixHttpApi {
* Generic O should be inferred
*/
private doRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>,
callback: Callback<T> | undefined,
method: Method,
uri: string,
queryParams?: Record<string, string>,
+1 -1
View File
@@ -20,7 +20,7 @@ import { IContent, MatrixEvent } from "./event";
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
import { EventTimeline } from "./event-timeline";
import { FileType } from "../http-api";
import type { ISendEventResponse } from "..";
import type { ISendEventResponse } from "../@types/requests";
/**
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
+2 -1
View File
@@ -287,7 +287,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* @return {?module:models/event-timeline~EventTimeline} timeline containing
* the given event, or null if unknown
*/
public getTimelineForEvent(eventId: string): EventTimeline | null {
public getTimelineForEvent(eventId: string | null): EventTimeline | null {
if (eventId === null) { return null; }
const res = this._eventIdToTimeline.get(eventId);
return (res === undefined) ? null : res;
}
+6 -2
View File
@@ -151,7 +151,7 @@ interface IKeyRequestRecipient {
export interface IDecryptOptions {
emit?: boolean;
isRetry?: boolean;
keyTrusted?: boolean;
forceRedecryptIfUntrusted?: boolean;
}
/**
@@ -678,6 +678,8 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
* @param {object} options
* @param {boolean} options.isRetry True if this is a retry (enables more logging)
* @param {boolean} options.emit Emits "event.decrypted" if set to true
* @param {boolean} options.forceRedecryptIfUntrusted whether the message should be
* re-decrypted if it was previously successfully decrypted with an untrusted key
*
* @returns {Promise} promise which resolves (to undefined) when the decryption
* attempt is completed.
@@ -696,7 +698,9 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
throw new Error("Attempt to decrypt event which isn't encrypted");
}
if (this.clearEvent && !this.isDecryptionFailure() && !(this.isKeySourceUntrusted() && options.keyTrusted)) {
const alreadyDecrypted = this.clearEvent && !this.isDecryptionFailure();
const forceRedecrypt = options.forceRedecryptIfUntrusted && this.isKeySourceUntrusted();
if (alreadyDecrypted && !forceRedecrypt) {
// we may want to just ignore this? let's start with rejecting it.
throw new Error(
"Attempt to decrypt event which has already been decrypted",
+306
View File
@@ -0,0 +1,306 @@
/*
Copyright 2022 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 { ReceiptType } from "../@types/read_receipts";
import { EventTimelineSet, EventType, MatrixEvent } from "../matrix";
import { ListenerMap, TypedEventEmitter } from "./typed-event-emitter";
import * as utils from "../utils";
export const MAIN_ROOM_TIMELINE = "main";
export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
return new MatrixEvent({
content: {
[event.getId()]: {
[receiptType]: {
[userId]: {
ts: event.getTs(),
threadId: event.threadRootId ?? MAIN_ROOM_TIMELINE,
},
},
},
},
type: EventType.Receipt,
room_id: event.getRoomId(),
});
}
export interface Receipt {
ts: number;
thread_id?: string;
}
export interface WrappedReceipt {
eventId: string;
data: Receipt;
}
interface CachedReceipt {
type: ReceiptType;
userId: string;
data: Receipt;
}
type ReceiptCache = {[eventId: string]: CachedReceipt[]};
export interface ReceiptContent {
[eventId: string]: {
[key in ReceiptType]: {
[userId: string]: Receipt;
};
};
}
const ReceiptPairRealIndex = 0;
const ReceiptPairSyntheticIndex = 1;
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
type Receipts = {
[receiptType: string]: {
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
};
};
export abstract class ReadReceipt<
Events extends string,
Arguments extends ListenerMap<Events>,
SuperclassArguments extends ListenerMap<any> = Arguments,
> extends TypedEventEmitter<Events, Arguments, SuperclassArguments> {
// receipts should clobber based on receipt_type and user_id pairs hence
// the form of this structure. This is sub-optimal for the exposed APIs
// which pass in an event ID and get back some receipts, so we also store
// a pre-cached list for this purpose.
private receipts: Receipts = {}; // { receipt_type: { user_id: Receipt } }
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: CachedReceipt[] }
public abstract getUnfilteredTimelineSet(): EventTimelineSet;
public abstract timeline: MatrixEvent[];
/**
* Gets the latest receipt for a given user in the room
* @param userId The id of the user for which we want the receipt
* @param ignoreSynthesized Whether to ignore synthesized receipts or not
* @param receiptType Optional. The type of the receipt we want to get
* @returns the latest receipts of the chosen type for the chosen user
*/
public getReadReceiptForUserId(
userId: string, ignoreSynthesized = false, receiptType = ReceiptType.Read,
): WrappedReceipt | null {
const [realReceipt, syntheticReceipt] = this.receipts[receiptType]?.[userId] ?? [];
if (ignoreSynthesized) {
return realReceipt;
}
return syntheticReceipt ?? realReceipt;
}
/**
* Get the ID of the event that a given user has read up to, or null if we
* have received no read receipts from them.
* @param {String} userId The user ID to get read receipt event ID for
* @param {Boolean} ignoreSynthesized If true, return only receipts that have been
* sent by the server, not implicit ones generated
* by the JS SDK.
* @return {String} ID of the latest event that the given user has read, or null.
*/
public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
// XXX: This is very very ugly and I hope I won't have to ever add a new
// receipt type here again. IMHO this should be done by the server in
// some more intelligent manner or the client should just use timestamps
const timelineSet = this.getUnfilteredTimelineSet();
const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read);
const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate);
// If we have both, compare them
let comparison: number | null | undefined;
if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) {
comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId);
}
// If we didn't get a comparison try to compare the ts of the receipts
if (!comparison && publicReadReceipt?.data?.ts && privateReadReceipt?.data?.ts) {
comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts;
}
// The public receipt is more likely to drift out of date so the private
// one has precedence
if (!comparison) return privateReadReceipt?.eventId ?? publicReadReceipt?.eventId ?? null;
// If public read receipt is older, return the private one
return ((comparison < 0) ? privateReadReceipt?.eventId : publicReadReceipt?.eventId) ?? null;
}
public addReceiptToStructure(
eventId: string,
receiptType: ReceiptType,
userId: string,
receipt: Receipt,
synthetic: boolean,
): void {
if (!this.receipts[receiptType]) {
this.receipts[receiptType] = {};
}
if (!this.receipts[receiptType][userId]) {
this.receipts[receiptType][userId] = [null, null];
}
const pair = this.receipts[receiptType][userId];
let existingReceipt = pair[ReceiptPairRealIndex];
if (synthetic) {
existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
}
if (existingReceipt) {
// we only want to add this receipt if we think it is later than the one we already have.
// This is managed server-side, but because we synthesize RRs locally we have to do it here too.
const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
existingReceipt.eventId,
eventId,
);
if (ordering !== null && ordering >= 0) {
return;
}
}
const wrappedReceipt: WrappedReceipt = {
eventId,
data: receipt,
};
const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt;
const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex];
let ordering: number | null = null;
if (realReceipt && syntheticReceipt) {
ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
realReceipt.eventId,
syntheticReceipt.eventId,
);
}
const preferSynthetic = ordering === null || ordering < 0;
// we don't bother caching just real receipts by event ID as there's nothing that would read it.
// Take the current cached receipt before we overwrite the pair elements.
const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
if (synthetic && preferSynthetic) {
pair[ReceiptPairSyntheticIndex] = wrappedReceipt;
} else if (!synthetic) {
pair[ReceiptPairRealIndex] = wrappedReceipt;
if (!preferSynthetic) {
pair[ReceiptPairSyntheticIndex] = null;
}
}
const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
if (cachedReceipt === newCachedReceipt) return;
// clean up any previous cache entry
if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) {
const previousEventId = cachedReceipt.eventId;
// Remove the receipt we're about to clobber out of existence from the cache
this.receiptCacheByEventId[previousEventId] = (
this.receiptCacheByEventId[previousEventId].filter(r => {
return r.type !== receiptType || r.userId !== userId;
})
);
if (this.receiptCacheByEventId[previousEventId].length < 1) {
delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys
}
}
// cache the new one
if (!this.receiptCacheByEventId[eventId]) {
this.receiptCacheByEventId[eventId] = [];
}
this.receiptCacheByEventId[eventId].push({
userId: userId,
type: receiptType as ReceiptType,
data: receipt,
});
}
/**
* Get a list of receipts for the given event.
* @param {MatrixEvent} event the event to get receipts for
* @return {Object[]} A list of receipts with a userId, type and data keys or
* an empty list.
*/
public getReceiptsForEvent(event: MatrixEvent): CachedReceipt[] {
return this.receiptCacheByEventId[event.getId()] || [];
}
public abstract addReceipt(event: MatrixEvent, synthetic: boolean): void;
/**
* Add a temporary local-echo receipt to the room to reflect in the
* client the fact that we've sent one.
* @param {string} userId The user ID if the receipt sender
* @param {MatrixEvent} e The event that is to be acknowledged
* @param {ReceiptType} receiptType The type of receipt
*/
public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void {
this.addReceipt(synthesizeReceipt(userId, e, receiptType), true);
}
/**
* Get a list of user IDs who have <b>read up to</b> the given event.
* @param {MatrixEvent} event the event to get read receipts for.
* @return {String[]} A list of user IDs.
*/
public getUsersReadUpTo(event: MatrixEvent): string[] {
return this.getReceiptsForEvent(event).filter(function(receipt) {
return utils.isSupportedReceiptType(receipt.type);
}).map(function(receipt) {
return receipt.userId;
});
}
/**
* Determines if the given user has read a particular event ID with the known
* history of the room. This is not a definitive check as it relies only on
* what is available to the room at the time of execution.
* @param {String} userId The user ID to check the read state of.
* @param {String} eventId The event ID to check if the user read.
* @returns {Boolean} True if the user has read the event, false otherwise.
*/
public hasUserReadEvent(userId: string, eventId: string): boolean {
const readUpToId = this.getEventReadUpTo(userId, false);
if (readUpToId === eventId) return true;
if (this.timeline.length
&& this.timeline[this.timeline.length - 1].getSender()
&& this.timeline[this.timeline.length - 1].getSender() === userId) {
// It doesn't matter where the event is in the timeline, the user has read
// it because they've sent the latest event.
return true;
}
for (let i = this.timeline.length - 1; i >= 0; --i) {
const ev = this.timeline[i];
// If we encounter the target event first, the user hasn't read it
// however if we encounter the readUpToId first then the user has read
// it. These rules apply because we're iterating bottom-up.
if (ev.getId() === eventId) return false;
if (ev.getId() === readUpToId) return true;
}
// We don't know if the user has read it, so assume not.
return false;
}
}
+47 -304
View File
@@ -47,10 +47,16 @@ import {
FILTER_RELATED_BY_SENDERS,
ThreadFilterType,
} from "./thread";
import { TypedEventEmitter } from "./typed-event-emitter";
import { ReceiptType } from "../@types/read_receipts";
import { IStateEventWithRoomId } from "../@types/search";
import { RelationsContainer } from "./relations-container";
import {
MAIN_ROOM_TIMELINE,
ReadReceipt,
Receipt,
ReceiptContent,
synthesizeReceipt,
} from "./read-receipt";
// These constants are used as sane defaults when the homeserver doesn't support
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
@@ -61,23 +67,6 @@ import { RelationsContainer } from "./relations-container";
export const KNOWN_SAFE_ROOM_VERSION = '9';
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
// console.log("synthesizing receipt for "+event.getId());
return new MatrixEvent({
content: {
[event.getId()]: {
[receiptType]: {
[userId]: {
ts: event.getTs(),
},
},
},
},
type: EventType.Receipt,
room_id: event.getRoomId(),
});
}
interface IOpts {
storageToken?: string;
pendingEventOrdering?: PendingEventOrdering;
@@ -91,40 +80,6 @@ export interface IRecommendedVersion {
urgent: boolean;
}
interface IReceipt {
ts: number;
}
export interface IWrappedReceipt {
eventId: string;
data: IReceipt;
}
interface ICachedReceipt {
type: ReceiptType;
userId: string;
data: IReceipt;
}
type ReceiptCache = {[eventId: string]: ICachedReceipt[]};
interface IReceiptContent {
[eventId: string]: {
[key in ReceiptType]: {
[userId: string]: IReceipt;
};
};
}
const ReceiptPairRealIndex = 0;
const ReceiptPairSyntheticIndex = 1;
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
type Receipts = {
[receiptType: string]: {
[userId: string]: [IWrappedReceipt, IWrappedReceipt]; // Pair<real receipt, synthetic receipt> (both nullable)
};
};
// When inserting a visibility event affecting event `eventId`, we
// need to scan through existing visibility events for `eventId`.
// In theory, this could take an unlimited amount of time if:
@@ -225,15 +180,9 @@ export type RoomEventHandlerMap = {
BeaconEvent.Update | BeaconEvent.Destroy | BeaconEvent.LivenessChange
>;
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
public readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
// receipts should clobber based on receipt_type and user_id pairs hence
// the form of this structure. This is sub-optimal for the exposed APIs
// which pass in an event ID and get back some receipts, so we also store
// a pre-cached list for this purpose.
private receipts: Receipts = {}; // { receipt_type: { user_id: IReceipt } }
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: ICachedReceipt[] }
private notificationCounts: Partial<Record<NotificationCountType, number>> = {};
private readonly timelineSets: EventTimelineSet[];
public readonly threadsTimelineSets: EventTimelineSet[] = [];
@@ -2400,10 +2349,10 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
threadId,
} = this.eventShouldLiveIn(event, events, threadRoots);
if (shouldLiveInThread && !eventsByThread[threadId]) {
eventsByThread[threadId] = [];
if (shouldLiveInThread && !eventsByThread[threadId ?? ""]) {
eventsByThread[threadId ?? ""] = [];
}
eventsByThread[threadId]?.push(event);
eventsByThread[threadId ?? ""]?.push(event);
if (shouldLiveInRoom) {
this.addLiveEvent(event, options);
@@ -2436,17 +2385,17 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
if (shouldLiveInThread) {
event.setThreadId(threadId);
event.setThreadId(threadId ?? "");
memo[THREAD].push(event);
}
return memo;
}, [[], []]);
}, [[] as MatrixEvent[], [] as MatrixEvent[]]);
} else {
// When `experimentalThreadSupport` is disabled treat all events as timelineEvents
return [
events,
[],
events as MatrixEvent[],
[] as MatrixEvent[],
];
}
}
@@ -2458,12 +2407,43 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
const threadRoots = new Set<string>();
for (const event of events) {
if (event.isRelation(THREAD_RELATION_TYPE.name)) {
threadRoots.add(event.relationEventId);
threadRoots.add(event.relationEventId ?? "");
}
}
return threadRoots;
}
/**
* Add a receipt event to the room.
* @param {MatrixEvent} event The m.receipt event.
* @param {Boolean} synthetic True if this event is implicit.
*/
public addReceipt(event: MatrixEvent, synthetic = false): void {
const content = event.getContent<ReceiptContent>();
Object.keys(content).forEach((eventId: string) => {
Object.keys(content[eventId]).forEach((receiptType: ReceiptType | string) => {
Object.keys(content[eventId][receiptType]).forEach((userId: string) => {
const receipt = content[eventId][receiptType][userId] as Receipt;
const receiptForMainTimeline = !receipt.thread_id || receipt.thread_id === MAIN_ROOM_TIMELINE;
const receiptDestination: Thread | this | undefined = receiptForMainTimeline
? this
: this.threads.get(receipt.thread_id ?? "");
receiptDestination?.addReceiptToStructure(
eventId,
receiptType as ReceiptType,
userId,
receipt,
synthetic,
);
});
});
});
// send events after we've regenerated the structure & cache, otherwise things that
// listened for the event would read stale data.
this.emit(RoomEvent.Receipt, event, this);
}
/**
* Adds/handles ephemeral events such as typing notifications and read receipts.
* @param {MatrixEvent[]} events A list of events to process
@@ -2554,243 +2534,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
}
/**
* Get a list of user IDs who have <b>read up to</b> the given event.
* @param {MatrixEvent} event the event to get read receipts for.
* @return {String[]} A list of user IDs.
*/
public getUsersReadUpTo(event: MatrixEvent): string[] {
return this.getReceiptsForEvent(event).filter(function(receipt) {
return utils.isSupportedReceiptType(receipt.type);
}).map(function(receipt) {
return receipt.userId;
});
}
/**
* Gets the latest receipt for a given user in the room
* @param userId The id of the user for which we want the receipt
* @param ignoreSynthesized Whether to ignore synthesized receipts or not
* @param receiptType Optional. The type of the receipt we want to get
* @returns the latest receipts of the chosen type for the chosen user
*/
public getReadReceiptForUserId(
userId: string, ignoreSynthesized = false, receiptType = ReceiptType.Read,
): IWrappedReceipt | null {
const [realReceipt, syntheticReceipt] = this.receipts[receiptType]?.[userId] ?? [];
if (ignoreSynthesized) {
return realReceipt;
}
return syntheticReceipt ?? realReceipt;
}
/**
* Get the ID of the event that a given user has read up to, or null if we
* have received no read receipts from them.
* @param {String} userId The user ID to get read receipt event ID for
* @param {Boolean} ignoreSynthesized If true, return only receipts that have been
* sent by the server, not implicit ones generated
* by the JS SDK.
* @return {String} ID of the latest event that the given user has read, or null.
*/
public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
// XXX: This is very very ugly and I hope I won't have to ever add a new
// receipt type here again. IMHO this should be done by the server in
// some more intelligent manner or the client should just use timestamps
const timelineSet = this.getUnfilteredTimelineSet();
const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read);
const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate);
// If we have both, compare them
let comparison: number | null | undefined;
if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) {
comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId);
}
// If we didn't get a comparison try to compare the ts of the receipts
if (!comparison && publicReadReceipt?.data?.ts && privateReadReceipt?.data?.ts) {
comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts;
}
// The public receipt is more likely to drift out of date so the private
// one has precedence
if (!comparison) return privateReadReceipt?.eventId ?? publicReadReceipt?.eventId ?? null;
// If public read receipt is older, return the private one
return ((comparison < 0) ? privateReadReceipt?.eventId : publicReadReceipt?.eventId) ?? null;
}
/**
* Determines if the given user has read a particular event ID with the known
* history of the room. This is not a definitive check as it relies only on
* what is available to the room at the time of execution.
* @param {String} userId The user ID to check the read state of.
* @param {String} eventId The event ID to check if the user read.
* @returns {Boolean} True if the user has read the event, false otherwise.
*/
public hasUserReadEvent(userId: string, eventId: string): boolean {
const readUpToId = this.getEventReadUpTo(userId, false);
if (readUpToId === eventId) return true;
if (this.timeline.length
&& this.timeline[this.timeline.length - 1].getSender()
&& this.timeline[this.timeline.length - 1].getSender() === userId) {
// It doesn't matter where the event is in the timeline, the user has read
// it because they've sent the latest event.
return true;
}
for (let i = this.timeline.length - 1; i >= 0; --i) {
const ev = this.timeline[i];
// If we encounter the target event first, the user hasn't read it
// however if we encounter the readUpToId first then the user has read
// it. These rules apply because we're iterating bottom-up.
if (ev.getId() === eventId) return false;
if (ev.getId() === readUpToId) return true;
}
// We don't know if the user has read it, so assume not.
return false;
}
/**
* Get a list of receipts for the given event.
* @param {MatrixEvent} event the event to get receipts for
* @return {Object[]} A list of receipts with a userId, type and data keys or
* an empty list.
*/
public getReceiptsForEvent(event: MatrixEvent): ICachedReceipt[] {
return this.receiptCacheByEventId[event.getId()] || [];
}
/**
* Add a receipt event to the room.
* @param {MatrixEvent} event The m.receipt event.
* @param {Boolean} synthetic True if this event is implicit.
*/
public addReceipt(event: MatrixEvent, synthetic = false): void {
this.addReceiptsToStructure(event, synthetic);
// send events after we've regenerated the structure & cache, otherwise things that
// listened for the event would read stale data.
this.emit(RoomEvent.Receipt, event, this);
}
/**
* Add a receipt event to the room.
* @param {MatrixEvent} event The m.receipt event.
* @param {Boolean} synthetic True if this event is implicit.
*/
private addReceiptsToStructure(event: MatrixEvent, synthetic: boolean): void {
const content = event.getContent<IReceiptContent>();
Object.keys(content).forEach((eventId) => {
Object.keys(content[eventId]).forEach((receiptType) => {
Object.keys(content[eventId][receiptType]).forEach((userId) => {
const receipt = content[eventId][receiptType][userId];
if (!this.receipts[receiptType]) {
this.receipts[receiptType] = {};
}
if (!this.receipts[receiptType][userId]) {
this.receipts[receiptType][userId] = [null, null];
}
const pair = this.receipts[receiptType][userId];
let existingReceipt = pair[ReceiptPairRealIndex];
if (synthetic) {
existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
}
if (existingReceipt) {
// we only want to add this receipt if we think it is later than the one we already have.
// This is managed server-side, but because we synthesize RRs locally we have to do it here too.
const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
existingReceipt.eventId,
eventId,
);
if (ordering !== null && ordering >= 0) {
return;
}
}
const wrappedReceipt: IWrappedReceipt = {
eventId,
data: receipt,
};
const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt;
const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex];
let ordering: number | null = null;
if (realReceipt && syntheticReceipt) {
ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
realReceipt.eventId,
syntheticReceipt.eventId,
);
}
const preferSynthetic = ordering === null || ordering < 0;
// we don't bother caching just real receipts by event ID as there's nothing that would read it.
// Take the current cached receipt before we overwrite the pair elements.
const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
if (synthetic && preferSynthetic) {
pair[ReceiptPairSyntheticIndex] = wrappedReceipt;
} else if (!synthetic) {
pair[ReceiptPairRealIndex] = wrappedReceipt;
if (!preferSynthetic) {
pair[ReceiptPairSyntheticIndex] = null;
}
}
const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
if (cachedReceipt === newCachedReceipt) return;
// clean up any previous cache entry
if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) {
const previousEventId = cachedReceipt.eventId;
// Remove the receipt we're about to clobber out of existence from the cache
this.receiptCacheByEventId[previousEventId] = (
this.receiptCacheByEventId[previousEventId].filter(r => {
return r.type !== receiptType || r.userId !== userId;
})
);
if (this.receiptCacheByEventId[previousEventId].length < 1) {
delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys
}
}
// cache the new one
if (!this.receiptCacheByEventId[eventId]) {
this.receiptCacheByEventId[eventId] = [];
}
this.receiptCacheByEventId[eventId].push({
userId: userId,
type: receiptType as ReceiptType,
data: receipt,
});
});
});
});
}
/**
* Add a temporary local-echo receipt to the room to reflect in the
* client the fact that we've sent one.
* @param {string} userId The user ID if the receipt sender
* @param {MatrixEvent} e The event that is to be acknowledged
* @param {ReceiptType} receiptType The type of receipt
*/
public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void {
this.addReceipt(synthesizeReceipt(userId, e, receiptType), true);
}
/**
* Update the room-tag event for the room. The previous one is overwritten.
* @param {MatrixEvent} event the m.tag event
+14 -2
View File
@@ -23,10 +23,10 @@ import { IThreadBundledRelationship, MatrixEvent } from "./event";
import { Direction, EventTimeline } from "./event-timeline";
import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set';
import { Room } from './room';
import { TypedEventEmitter } from "./typed-event-emitter";
import { RoomState } from "./room-state";
import { ServerControlledNamespacedValue } from "../NamespacedValue";
import { logger } from "../logger";
import { ReadReceipt } from "./read-receipt";
export enum ThreadEvent {
New = "Thread.new",
@@ -54,7 +54,7 @@ interface IThreadOpts {
/**
* @experimental
*/
export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
public static hasServerSideSupport: boolean;
/**
@@ -429,6 +429,18 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
nextBatch,
};
}
public getUnfilteredTimelineSet(): EventTimelineSet {
return this.timelineSet;
}
public get timeline(): MatrixEvent[] {
return this.events;
}
public addReceipt(event: MatrixEvent, synthetic: boolean): void {
throw new Error("Unsupported function on the thread model");
}
}
export const FILTER_RELATED_BY_SENDERS = new ServerControlledNamespacedValue(
+1 -1
View File
@@ -162,7 +162,7 @@ export interface IStore {
* Get account data event by event type
* @param {string} eventType The event type being queried
*/
getAccountData(eventType: EventType | string): MatrixEvent;
getAccountData(eventType: EventType | string): MatrixEvent | undefined;
/**
* setSyncData does nothing as there is no backing data store.
+1 -1
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { ISavedSync } from "./index";
import { IEvent, IStartClientOpts, IStateEventWithRoomId, ISyncResponse } from "..";
import { IEvent, IStartClientOpts, IStateEventWithRoomId, ISyncResponse } from "../matrix";
import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
export interface IIndexedDBBackend {
+29 -41
View File
@@ -18,41 +18,36 @@ import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../syn
import * as utils from "../utils";
import * as IndexedDBHelpers from "../indexeddb-helpers";
import { logger } from '../logger';
import { IStartClientOpts, IStateEventWithRoomId } from "..";
import { IStartClientOpts, IStateEventWithRoomId } from "../matrix";
import { ISavedSync } from "./index";
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
const VERSION = 4;
type DbMigration = (db: IDBDatabase) => void;
const DB_MIGRATIONS: DbMigration[] = [
(db) => {
// Make user store, clobber based on user ID. (userId property of User objects)
db.createObjectStore("users", { keyPath: ["userId"] });
function createDatabase(db: IDBDatabase): void {
// Make user store, clobber based on user ID. (userId property of User objects)
db.createObjectStore("users", { keyPath: ["userId"] });
// Make account data store, clobber based on event type.
// (event.type property of MatrixEvent objects)
db.createObjectStore("accountData", { keyPath: ["type"] });
// Make account data store, clobber based on event type.
// (event.type property of MatrixEvent objects)
db.createObjectStore("accountData", { keyPath: ["type"] });
// Make /sync store (sync tokens, room data, etc), always clobber (const key).
db.createObjectStore("sync", { keyPath: ["clobber"] });
}
function upgradeSchemaV2(db: IDBDatabase): void {
const oobMembersStore = db.createObjectStore(
"oob_membership_events", {
keyPath: ["room_id", "state_key"],
});
oobMembersStore.createIndex("room", "room_id");
}
function upgradeSchemaV3(db: IDBDatabase): void {
db.createObjectStore("client_options",
{ keyPath: ["clobber"] });
}
function upgradeSchemaV4(db: IDBDatabase): void {
db.createObjectStore("to_device_queue", { autoIncrement: true });
}
// Make /sync store (sync tokens, room data, etc), always clobber (const key).
db.createObjectStore("sync", { keyPath: ["clobber"] });
},
(db) => {
const oobMembersStore = db.createObjectStore(
"oob_membership_events", {
keyPath: ["room_id", "state_key"],
});
oobMembersStore.createIndex("room", "room_id");
},
(db) => { db.createObjectStore("client_options", { keyPath: ["clobber"] }); },
(db) => { db.createObjectStore("to_device_queue", { autoIncrement: true }); },
// Expand as needed.
];
const VERSION = DB_MIGRATIONS.length;
/**
* Helper method to collect results from a Cursor and promiseify it.
@@ -172,20 +167,13 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
logger.log(
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
);
if (oldVersion < 1) { // The database did not previously exist.
if (oldVersion < 1) {
// The database did not previously exist
this._isNewlyCreated = true;
createDatabase(db);
}
if (oldVersion < 2) {
upgradeSchemaV2(db);
}
if (oldVersion < 3) {
upgradeSchemaV3(db);
}
if (oldVersion < 4) {
upgradeSchemaV4(db);
}
// Expand as needed.
DB_MIGRATIONS.forEach((migration, index) => {
if (oldVersion <= index) migration(db);
});
};
req.onblocked = () => {
+1 -1
View File
@@ -18,7 +18,7 @@ import { logger } from "../logger";
import { defer, IDeferred } from "../utils";
import { ISavedSync } from "./index";
import { IStartClientOpts } from "../client";
import { IStateEventWithRoomId, ISyncResponse } from "..";
import { IStateEventWithRoomId, ISyncResponse } from "../matrix";
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
+8 -8
View File
@@ -66,7 +66,7 @@ interface IState {
export interface ITimeline {
events: Array<IRoomEvent | IStateEvent>;
limited?: boolean;
prev_batch: string;
prev_batch: string | null;
}
export interface IJoinedRoom {
@@ -401,7 +401,7 @@ export class SyncAccumulator {
// typing forever until someone really does start typing (which
// will prompt Synapse to send down an actual m.typing event to
// clobber the one we persisted).
if (e.type !== "m.receipt" || !e.content) {
if (e.type !== EventType.Receipt || !e.content) {
// This means we'll drop unknown ephemeral events but that
// seems okay.
return;
@@ -528,7 +528,7 @@ export class SyncAccumulator {
});
Object.keys(this.joinRooms).forEach((roomId) => {
const roomData = this.joinRooms[roomId];
const roomJson = {
const roomJson: IJoinedRoom = {
ephemeral: { events: [] },
account_data: { events: [] },
state: { events: [] },
@@ -541,12 +541,12 @@ export class SyncAccumulator {
};
// Add account data
Object.keys(roomData._accountData).forEach((evType) => {
roomJson.account_data.events.push(roomData._accountData[evType]);
roomJson.account_data.events.push(roomData._accountData[evType] as IMinimalEvent);
});
// Add receipt data
const receiptEvent = {
type: "m.receipt",
type: EventType.Receipt,
room_id: roomId,
content: {
// $event_id: { "m.read": { $user_id: $json } }
@@ -566,7 +566,7 @@ export class SyncAccumulator {
});
// add only if we have some receipt data
if (Object.keys(receiptEvent.content).length > 0) {
roomJson.ephemeral.events.push(receiptEvent);
roomJson.ephemeral.events.push(receiptEvent as IMinimalEvent);
}
// Add timeline data
@@ -609,8 +609,8 @@ export class SyncAccumulator {
const rollBackState = Object.create(null);
for (let i = roomJson.timeline.events.length - 1; i >=0; i--) {
const timelineEvent = roomJson.timeline.events[i];
if (timelineEvent.state_key === null ||
timelineEvent.state_key === undefined) {
if ((timelineEvent as IStateEvent).state_key === null ||
(timelineEvent as IStateEvent).state_key === undefined) {
continue; // not a state event
}
// since we're going back in time, we need to use the previous
+3 -1
View File
@@ -1269,7 +1269,9 @@ export class SyncApi {
if (joinObj.isBrandNewRoom) {
// set the back-pagination token. Do this *before* adding any
// events so that clients can start back-paginating.
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
if (joinObj.timeline.prev_batch !== null) {
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
}
} else if (joinObj.timeline.limited) {
let limited = true;
+1 -1
View File
@@ -24,7 +24,7 @@ import unhomoglyph from "unhomoglyph";
import promiseRetry from "p-retry";
import type * as NodeCrypto from "crypto";
import { MatrixEvent } from ".";
import { MatrixEvent } from "./models/event";
import { M_TIMESTAMP } from "./@types/location";
import { ReceiptType } from "./@types/read_receipts";
+730 -649
View File
File diff suppressed because it is too large Load Diff