Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 121250a6fb | |||
| a7aa227f55 | |||
| 04c1dfe43a | |||
| 07476a0ae0 | |||
| 6348704bec | |||
| f84a33910c | |||
| 0ccf7c50f2 | |||
| ead33003b7 | |||
| 7d5360a00f | |||
| 4b283015ba | |||
| 887e15aac5 | |||
| a83d80f502 | |||
| 5afe373446 | |||
| 9bb5afe5c0 | |||
| f349663329 | |||
| f398e3564d | |||
| 91171afddd | |||
| f410e71bfa | |||
| 83fca5b57d | |||
| 4ba083e6af | |||
| 0403e4bedc | |||
| 14aa7846a5 | |||
| 45348a354e | |||
| fa3339fc84 | |||
| b332c6c4b9 | |||
| 209a101be7 | |||
| efbf5479d1 | |||
| dacef048be | |||
| caadc6f95b | |||
| 39bc7e2bb3 | |||
| 040f012350 | |||
| 8b2b677f92 | |||
| 2b1fab928b | |||
| 516f52c5a4 | |||
| 8599a98b47 | |||
| 7e24cb6cae | |||
| 38aa8d18c0 | |||
| 2967ee6309 | |||
| 2e10b6065c | |||
| 69057ee035 |
+21
-1
@@ -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: [
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
Executable
+17
@@ -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];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user