1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-30 04:23:07 +03:00

Enable noImplicitAny (#2895)

* Stash noImplicitAny work

* Enable noImplicitAny

* Update olm

* Fun

* Fix msgid stuff

* Fix tests

* Attempt to fix Browserify
This commit is contained in:
Michael Telatynski
2022-12-06 18:21:44 +00:00
committed by GitHub
parent 6f81371e61
commit 8d018f9c2d
83 changed files with 1615 additions and 1428 deletions

View File

@ -64,7 +64,7 @@ jobs:
- name: Generate Docs - name: Generate Docs
run: "yarn run gendoc" run: "yarn run gendoc"
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
@ -72,38 +72,3 @@ jobs:
path: _docs path: _docs
# We'll only use this in a workflow_run, then we're done with it # We'll only use this in a workflow_run, then we're done with it
retention-days: 1 retention-days: 1
tsc-strict:
name: Typescript Strict Error Checker
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: read
checks: write
steps:
- uses: actions/checkout@v3
- name: Get diff lines
id: diff
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
with:
include: '["\\.tsx?$"]'
- name: Detecting files changed
id: files
uses: futuratrepadeira/changed-files@v4.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pattern: '^.*\.tsx?$'
- uses: t3chguy/typescript-check-action@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
use-check: false
check-fail-mode: added
output-behaviour: annotate
ts-extra-args: '--noImplicitAny'
files-changed: ${{ steps.files.outputs.files_updated }}
files-added: ${{ steps.files.outputs.files_created }}
files-deleted: ${{ steps.files.outputs.files_deleted }}
line-numbers: ${{ steps.diff.outputs.lineNumbers }}

View File

@ -14,7 +14,7 @@
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", "build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly", "build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src", "build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js", "build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "typedoc", "gendoc": "typedoc",
"lint": "yarn lint:types && yarn lint:js", "lint": "yarn lint:types && yarn lint:js",
@ -33,9 +33,9 @@
"matrix-org" "matrix-org"
], ],
"main": "./src/index.ts", "main": "./src/index.ts",
"browser": "./lib/browser-index.js", "browser": "./lib/browser-index.ts",
"matrix_src_main": "./src/index.ts", "matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.js", "matrix_src_browser": "./src/browser-index.ts",
"matrix_lib_main": "./lib/index.js", "matrix_lib_main": "./lib/index.js",
"matrix_lib_typings": "./lib/index.d.ts", "matrix_lib_typings": "./lib/index.d.ts",
"author": "matrix.org", "author": "matrix.org",
@ -80,7 +80,7 @@
"@babel/preset-typescript": "^7.12.7", "@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10", "@babel/register": "^7.12.10",
"@casualbot/jest-sonar-reporter": "^2.2.5", "@casualbot/jest-sonar-reporter": "^2.2.5",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
"@types/bs58": "^4.0.1", "@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5", "@types/content-type": "^1.1.5",
"@types/domexception": "^4.0.0", "@types/domexception": "^4.0.0",

View File

@ -183,7 +183,7 @@ export class TestClient {
this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>( this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>(
200, (_path, content) => { 200, (_path, content) => {
Object.keys(response.device_keys).forEach((userId) => { Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys![userId]).toEqual([]); expect((content.device_keys! as Record<string, any>)[userId]).toEqual([]);
}); });
return response; return response;
}); });

View File

@ -15,19 +15,7 @@ limitations under the License.
*/ */
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import type { MatrixClient, ClientEvent } from "../../src"; import type { default as BrowserMatrix } from "../../src/browser-index";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface Global {
matrixcs: {
MatrixClient: typeof MatrixClient;
ClientEvent: typeof ClientEvent;
};
}
}
}
// stub for browser-matrix browserify tests // stub for browser-matrix browserify tests
// @ts-ignore // @ts-ignore
@ -43,4 +31,4 @@ afterAll(() => {
global.matrixcs = { global.matrixcs = {
...global.matrixcs, ...global.matrixcs,
timeoutSignal: () => new AbortController().signal, timeoutSignal: () => new AbortController().signal,
}; } as typeof BrowserMatrix;

View File

@ -16,7 +16,7 @@ limitations under the License.
import HttpBackend from "matrix-mock-request"; import HttpBackend from "matrix-mock-request";
import "./setupTests";// uses browser-matrix instead of the src import "./setupTests"; // uses browser-matrix instead of the src
import type { MatrixClient } from "../../src"; import type { MatrixClient } from "../../src";
const USER_ID = "@user:test.server"; const USER_ID = "@user:test.server";
@ -65,15 +65,16 @@ describe("Browserify Test", function() {
const syncData = { const syncData = {
next_batch: "batch1", next_batch: "batch1",
rooms: { rooms: {
join: {}, join: {
}, [ROOM_ID]: {
}; timeline: {
syncData.rooms.join[ROOM_ID] = { events: [
timeline: { event,
events: [ ],
event, limited: false,
], },
limited: false, },
},
}, },
}; };

View File

@ -30,7 +30,7 @@ const ROOM_ID = "!room:id";
* *
* @return {object} sync response * @return {object} sync response
*/ */
function getSyncResponse(roomMembers) { function getSyncResponse(roomMembers: string[]) {
const stateEvents = [ const stateEvents = [
testUtils.mkEvent({ testUtils.mkEvent({
type: 'm.room.encryption', type: 'm.room.encryption',
@ -43,12 +43,10 @@ function getSyncResponse(roomMembers) {
Array.prototype.push.apply( Array.prototype.push.apply(
stateEvents, stateEvents,
roomMembers.map( roomMembers.map((m) => testUtils.mkMembership({
(m) => testUtils.mkMembership({ mship: 'join',
mship: 'join', sender: m,
sender: m, })),
}),
),
); );
const syncResponse = { const syncResponse = {
@ -73,8 +71,8 @@ describe("DeviceList management:", function() {
return; return;
} }
let sessionStoreBackend; let aliceTestClient: TestClient;
let aliceTestClient; let sessionStoreBackend: Storage;
async function createTestClient() { async function createTestClient() {
const testClient = new TestClient( const testClient = new TestClient(
@ -97,7 +95,10 @@ describe("DeviceList management:", function() {
}); });
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() { it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } }); aliceTestClient.expectKeyQuery({
device_keys: { '@alice:localhost': {} },
failures: {},
});
return aliceTestClient.start().then(function() { return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']); const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse); aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
@ -138,7 +139,10 @@ describe("DeviceList management:", function() {
it.skip("We should not get confused by out-of-order device query responses", () => { it.skip("We should not get confused by out-of-order device query responses", () => {
// https://github.com/vector-im/element-web/issues/3126 // https://github.com/vector-im/element-web/issues/3126
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } }); aliceTestClient.expectKeyQuery({
device_keys: { '@alice:localhost': {} },
failures: {},
});
return aliceTestClient.start().then(() => { return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond( aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc'])); 200, getSyncResponse(['@bob:xyz', '@chris:abc']));
@ -164,11 +168,12 @@ describe("DeviceList management:", function() {
aliceTestClient.httpBackend.flush('/keys/query', 1).then( aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1), () => aliceTestClient.httpBackend.flush('/send/', 1),
), ),
aliceTestClient.client.crypto.deviceList.saveIfDirty(), aliceTestClient.client.crypto!.deviceList.saveIfDirty(),
]); ]);
}).then(() => { }).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { // @ts-ignore accessing a protected field
expect(data.syncToken).toEqual(1); aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
expect(data!.syncToken).toEqual(1);
}); });
// invalidate bob's and chris's device lists in separate syncs // invalidate bob's and chris's device lists in separate syncs
@ -201,15 +206,16 @@ describe("DeviceList management:", function() {
return aliceTestClient.httpBackend.flush('/keys/query', 1); return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => { }).then((flushed) => {
expect(flushed).toEqual(0); expect(flushed).toEqual(0);
return aliceTestClient.client.crypto.deviceList.saveIfDirty(); return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
}).then(() => { }).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { // @ts-ignore accessing a protected field
const bobStat = data.trackingStatus['@bob:xyz']; aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) { if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' + throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat); bobStat);
} }
const chrisStat = data.trackingStatus['@chris:abc']; const chrisStat = data!.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) { if (chrisStat != 1 && chrisStat != 2) {
throw new Error( throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat, 'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
@ -234,12 +240,13 @@ describe("DeviceList management:", function() {
// wait for the client to stop processing the response // wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']); return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => { }).then(() => {
return aliceTestClient.client.crypto.deviceList.saveIfDirty(); return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
}).then(() => { }).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { // @ts-ignore accessing a protected field
const bobStat = data.trackingStatus['@bob:xyz']; aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3); expect(bobStat).toEqual(3);
const chrisStat = data.trackingStatus['@chris:abc']; const chrisStat = data!.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) { if (chrisStat != 1 && chrisStat != 2) {
throw new Error( throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat, 'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
@ -255,15 +262,16 @@ describe("DeviceList management:", function() {
// wait for the client to stop processing the response // wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']); return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => { }).then(() => {
return aliceTestClient.client.crypto.deviceList.saveIfDirty(); return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
}).then(() => { }).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { // @ts-ignore accessing a protected field
const bobStat = data.trackingStatus['@bob:xyz']; aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const chrisStat = data.trackingStatus['@bob:xyz']; const bobStat = data!.trackingStatus['@bob:xyz'];
const chrisStat = data!.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3); expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3); expect(chrisStat).toEqual(3);
expect(data.syncToken).toEqual(3); expect(data!.syncToken).toEqual(3);
}); });
}); });
}); });
@ -285,10 +293,11 @@ describe("DeviceList management:", function() {
}, },
); );
await aliceTestClient.httpBackend.flush('/keys/query', 1); await aliceTestClient.httpBackend.flush('/keys/query', 1);
await aliceTestClient.client.crypto.deviceList.saveIfDirty(); await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { // @ts-ignore accessing a protected field
const bobStat = data.trackingStatus['@bob:xyz']; aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
// Alice should be tracking bob's device list // Alice should be tracking bob's device list
expect(bobStat).toBeGreaterThan( expect(bobStat).toBeGreaterThan(
@ -322,10 +331,11 @@ describe("DeviceList management:", function() {
); );
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
await aliceTestClient.client.crypto.deviceList.saveIfDirty(); await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { // @ts-ignore accessing a protected field
const bobStat = data.trackingStatus['@bob:xyz']; aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked // Alice should have marked bob's device list as untracked
expect(bobStat).toEqual( expect(bobStat).toEqual(
@ -359,15 +369,14 @@ describe("DeviceList management:", function() {
); );
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
await aliceTestClient.client.crypto.deviceList.saveIfDirty(); await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { // @ts-ignore accessing a protected field
const bobStat = data.trackingStatus['@bob:xyz']; aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked // Alice should have marked bob's device list as untracked
expect(bobStat).toEqual( expect(bobStat).toEqual(0);
0,
);
}); });
}); });
@ -388,9 +397,7 @@ describe("DeviceList management:", function() {
const bobStat = data!.trackingStatus['@bob:xyz']; const bobStat = data!.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked // Alice should have marked bob's device list as untracked
expect(bobStat).toEqual( expect(bobStat).toEqual(0);
0,
);
}); });
} finally { } finally {
anotherTestClient.stop(); anotherTestClient.stop();

View File

@ -28,12 +28,14 @@ limitations under the License.
// load olm before the sdk if possible // load olm before the sdk if possible
import '../olm-loader'; import '../olm-loader';
import type { Session } from "@matrix-org/olm";
import { logger } from '../../src/logger'; import { logger } from '../../src/logger';
import * as testUtils from "../test-utils/test-utils"; import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client"; import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix"; import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
import { DeviceInfo } from '../../src/crypto/deviceinfo'; import { DeviceInfo } from '../../src/crypto/deviceinfo';
import { IDeviceKeys, IOneTimeKey } from "../../src/crypto/dehydration";
let aliTestClient: TestClient; let aliTestClient: TestClient;
const roomId = "!room:localhost"; const roomId = "!room:localhost";
@ -47,11 +49,7 @@ const bobAccessToken = "fewgfkuesa";
let aliMessages: IContent[]; let aliMessages: IContent[];
let bobMessages: IContent[]; let bobMessages: IContent[];
// IMessage isn't exported by src/crypto/algorithms/olm.ts type OlmPayload = ReturnType<Session["encrypt"]>;
interface OlmPayload {
type: number;
body: string;
}
async function bobUploadsDeviceKeys(): Promise<void> { async function bobUploadsDeviceKeys(): Promise<void> {
bobTestClient.expectDeviceKeyUpload(); bobTestClient.expectDeviceKeyUpload();
@ -71,12 +69,12 @@ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<num
// can't query keys before bob has uploaded them // can't query keys before bob has uploaded them
expect(uploader.deviceKeys).toBeTruthy(); expect(uploader.deviceKeys).toBeTruthy();
const uploaderKeys = {}; const uploaderKeys: Record<string, IDeviceKeys> = {};
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys; uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!;
querier.httpBackend.when("POST", "/keys/query") querier.httpBackend.when("POST", "/keys/query")
.respond(200, function(_path, content: IUploadKeysRequest) { .respond(200, function(_path, content: IQueryKeysRequest) {
expect(content.device_keys![uploader.userId!]).toEqual([]); expect(content.device_keys![uploader.userId!]).toEqual([]);
const result = {}; const result: Record<string, Record<string, IDeviceKeys>> = {};
result[uploader.userId!] = uploaderKeys; result[uploader.userId!] = uploaderKeys;
return { device_keys: result }; return { device_keys: result };
}); });
@ -94,7 +92,7 @@ async function expectAliClaimKeys(): Promise<void> {
const keys = await bobTestClient.awaitOneTimeKeyUpload(); const keys = await bobTestClient.awaitOneTimeKeyUpload();
aliTestClient.httpBackend.when( aliTestClient.httpBackend.when(
"POST", "/keys/claim", "POST", "/keys/claim",
).respond(200, function(_path, content: IUploadKeysRequest) { ).respond(200, function(_path, content: IClaimKeysRequest) {
const claimType = content.one_time_keys![bobUserId][bobDeviceId]; const claimType = content.one_time_keys![bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519"); expect(claimType).toEqual("signed_curve25519");
let keyId = ''; let keyId = '';
@ -105,7 +103,7 @@ async function expectAliClaimKeys(): Promise<void> {
} }
} }
} }
const result = {}; const result: Record<string, Record<string, Record<string, IOneTimeKey>>> = {};
result[bobUserId] = {}; result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {}; result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = keys[keyId]; result[bobUserId][bobDeviceId][keyId] = keys[keyId];
@ -276,22 +274,21 @@ async function recvMessage(
next_batch: "x", next_batch: "x",
rooms: { rooms: {
join: { join: {
[roomId]: {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
},
}, },
}, },
}; };
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData); httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => { const eventPromise = new Promise<MatrixEvent>((resolve) => {
@ -335,24 +332,25 @@ function firstSync(testClient: TestClient): Promise<void> {
const syncData = { const syncData = {
next_batch: "x", next_batch: "x",
rooms: { rooms: {
join: { }, join: {
}, [roomId]: {
}; state: {
syncData.rooms.join[roomId] = { events: [
state: { testUtils.mkMembership({
events: [ mship: "join",
testUtils.mkMembership({ user: aliUserId,
mship: "join", }),
user: aliUserId, testUtils.mkMembership({
}), mship: "join",
testUtils.mkMembership({ user: bobUserId,
mship: "join", }),
user: bobUserId, ],
}), },
], timeline: {
}, events: [],
timeline: { },
events: [], },
},
}, },
}; };
@ -424,7 +422,7 @@ describe("MatrixClient crypto", () => {
}, },
}; };
const bobKeys = {}; const bobKeys: Record<string, typeof bobDeviceKeys> = {};
bobKeys[bobDeviceId] = bobDeviceKeys; bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when( aliTestClient.httpBackend.when(
"POST", "/keys/query", "POST", "/keys/query",
@ -460,7 +458,7 @@ describe("MatrixClient crypto", () => {
}, },
}; };
const bobKeys = {}; const bobKeys: Record<string, typeof bobDeviceKeys> = {};
bobKeys[bobDeviceId] = bobDeviceKeys; bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when( aliTestClient.httpBackend.when(
"POST", "/keys/query", "POST", "/keys/query",
@ -515,22 +513,21 @@ describe("MatrixClient crypto", () => {
next_batch: "x", next_batch: "x",
rooms: { rooms: {
join: { join: {
[roomId]: {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
},
}, },
}, },
}; };
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
};
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData); bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => { const eventPromise = new Promise<MatrixEvent>((resolve) => {
@ -607,20 +604,21 @@ describe("MatrixClient crypto", () => {
const syncData = { const syncData = {
next_batch: '2', next_batch: '2',
rooms: { rooms: {
join: {}, join: {
}, [roomId]: {
}; state: {
syncData.rooms.join[roomId] = { events: [
state: { testUtils.mkEvent({
events: [ type: 'm.room.encryption',
testUtils.mkEvent({ skey: '',
type: 'm.room.encryption', content: {
skey: '', algorithm: 'm.olm.v1.curve25519-aes-sha2',
content: { },
algorithm: 'm.olm.v1.curve25519-aes-sha2', }),
],
}, },
}), },
], },
}, },
}; };

View File

@ -536,20 +536,20 @@ describe("MatrixClient event timelines", function() {
}; };
}); });
let tl0; let tl0: EventTimeline;
let tl3; let tl3: EventTimeline;
return Promise.all([ return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id!, client.getEventTimeline(timelineSet, EVENTS[0].event_id!,
).then(function(tl) { ).then(function(tl) {
expect(tl!.getEvents().length).toEqual(1); expect(tl!.getEvents().length).toEqual(1);
tl0 = tl; tl0 = tl!;
return client.getEventTimeline(timelineSet, EVENTS[2].event_id!); return client.getEventTimeline(timelineSet, EVENTS[2].event_id!);
}).then(function(tl) { }).then(function(tl) {
expect(tl!.getEvents().length).toEqual(1); expect(tl!.getEvents().length).toEqual(1);
return client.getEventTimeline(timelineSet, EVENTS[3].event_id!); return client.getEventTimeline(timelineSet, EVENTS[3].event_id!);
}).then(function(tl) { }).then(function(tl) {
expect(tl!.getEvents().length).toEqual(1); expect(tl!.getEvents().length).toEqual(1);
tl3 = tl; tl3 = tl!;
return client.getEventTimeline(timelineSet, EVENTS[1].event_id!); return client.getEventTimeline(timelineSet, EVENTS[1].event_id!);
}).then(function(tl) { }).then(function(tl) {
// we expect it to get merged in with event 2 // we expect it to get merged in with event 2
@ -953,11 +953,11 @@ describe("MatrixClient event timelines", function() {
}; };
}); });
let tl; let tl: EventTimeline;
return Promise.all([ return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id!, client.getEventTimeline(timelineSet, EVENTS[0].event_id!,
).then(function(tl0) { ).then(function(tl0) {
tl = tl0; tl = tl0!;
return client.paginateEventTimeline(tl, { backwards: true }); return client.paginateEventTimeline(tl, { backwards: true });
}).then(function(success) { }).then(function(success) {
expect(success).toBeTruthy(); expect(success).toBeTruthy();
@ -1043,11 +1043,11 @@ describe("MatrixClient event timelines", function() {
}; };
}); });
let tl; let tl: EventTimeline;
return Promise.all([ return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id!, client.getEventTimeline(timelineSet, EVENTS[0].event_id!,
).then(function(tl0) { ).then(function(tl0) {
tl = tl0; tl = tl0!;
return client.paginateEventTimeline( return client.paginateEventTimeline(
tl, { backwards: false, limit: 20 }); tl, { backwards: false, limit: 20 });
}).then(function(success) { }).then(function(success) {
@ -1569,16 +1569,17 @@ describe("MatrixClient event timelines", function() {
const syncData = { const syncData = {
next_batch: "batch1", next_batch: "batch1",
rooms: { rooms: {
join: {}, join: {
}, [roomId]: {
}; timeline: {
syncData.rooms.join[roomId] = { events: [
timeline: { event,
events: [ redaction,
event, ],
redaction, limited: false,
], },
limited: false, },
},
}, },
}; };
httpBackend.when("GET", "/sync").respond(200, syncData); httpBackend.when("GET", "/sync").respond(200, syncData);
@ -1595,18 +1596,19 @@ describe("MatrixClient event timelines", function() {
const sync2 = { const sync2 = {
next_batch: "batch2", next_batch: "batch2",
rooms: { rooms: {
join: {}, join: {
}, [roomId]: {
}; timeline: {
sync2.rooms.join[roomId] = { events: [
timeline: { utils.mkMessage({
events: [ user: otherUserId, msg: "world",
utils.mkMessage({ }),
user: otherUserId, msg: "world", ],
}), limited: true,
], prev_batch: "newerTok",
limited: true, },
prev_batch: "newerTok", },
},
}, },
}; };
httpBackend.when("GET", "/sync").respond(200, sync2); httpBackend.when("GET", "/sync").respond(200, sync2);

View File

@ -618,13 +618,13 @@ describe("MatrixClient", function() {
}); });
describe("partitionThreadedEvents", function() { describe("partitionThreadedEvents", function() {
let room; let room: Room;
beforeEach(() => { beforeEach(() => {
room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client!, userId); room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client!, userId);
}); });
it("returns empty arrays when given an empty arrays", function() { it("returns empty arrays when given an empty arrays", function() {
const events = []; const events: MatrixEvent[] = [];
const [timeline, threaded] = room.partitionThreadedEvents(events); const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([]); expect(timeline).toEqual([]);
expect(threaded).toEqual([]); expect(threaded).toEqual([]);
@ -1645,7 +1645,7 @@ const buildEventCreate = () => new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org", "user_id": "@andybalaam-test1:matrix.org",
}); });
function assertObjectContains(obj: object, expected: any): void { function assertObjectContains(obj: Record<string, any>, expected: any): void {
for (const k in expected) { for (const k in expected) {
if (expected.hasOwnProperty(k)) { if (expected.hasOwnProperty(k)) {
expect(obj[k]).toEqual(expected[k]); expect(obj[k]).toEqual(expected[k]);

View File

@ -1,7 +1,7 @@
import HttpBackend from "matrix-mock-request"; import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { MatrixClient } from "../../src/matrix"; import { ClientEvent, MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler"; import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory"; import { MemoryStore } from "../../src/store/memory";
import { MatrixError } from "../../src/http-api"; import { MatrixError } from "../../src/http-api";
@ -65,7 +65,7 @@ describe("MatrixClient opts", function() {
}); });
describe("without opts.store", function() { describe("without opts.store", function() {
let client; let client: MatrixClient;
beforeEach(function() { beforeEach(function() {
client = new MatrixClient({ client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch, fetchFn: httpBackend.fetchFn as typeof global.fetch,
@ -98,7 +98,7 @@ describe("MatrixClient opts", function() {
"m.room.message", "m.room.name", "m.room.member", "m.room.member", "m.room.message", "m.room.name", "m.room.member", "m.room.member",
"m.room.create", "m.room.create",
]; ];
client.on("event", function(event) { client.on(ClientEvent.Event, function(event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual( expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
-1, -1,
); );
@ -125,7 +125,7 @@ describe("MatrixClient opts", function() {
}); });
describe("without opts.scheduler", function() { describe("without opts.scheduler", function() {
let client; let client: MatrixClient;
beforeEach(function() { beforeEach(function() {
client = new MatrixClient({ client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch, fetchFn: httpBackend.fetchFn as typeof global.fetch,

View File

@ -18,7 +18,16 @@ import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { EventStatus } from "../../src/models/event"; import { EventStatus } from "../../src/models/event";
import { MatrixError, ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src"; import {
MatrixError,
ClientEvent,
IEvent,
MatrixClient,
RoomEvent,
ISyncResponse,
IMinimalEvent,
IRoomEvent, Room,
} from "../../src";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
describe("MatrixClient room timelines", function() { describe("MatrixClient room timelines", function() {
@ -39,7 +48,7 @@ describe("MatrixClient room timelines", function() {
name: "Old room name", name: "Old room name",
}, },
}); });
let NEXT_SYNC_DATA; let NEXT_SYNC_DATA: Partial<ISyncResponse>;
const SYNC_DATA = { const SYNC_DATA = {
next_batch: "s_5_3", next_batch: "s_5_3",
rooms: { rooms: {
@ -88,7 +97,7 @@ describe("MatrixClient room timelines", function() {
}, },
}, },
leave: {}, leave: {},
}, } as unknown as ISyncResponse["rooms"],
}; };
events.forEach(function(e) { events.forEach(function(e) {
if (e.room_id !== roomId) { if (e.room_id !== roomId) {
@ -96,11 +105,11 @@ describe("MatrixClient room timelines", function() {
} }
if (e.state_key) { if (e.state_key) {
// push the current // push the current
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e); NEXT_SYNC_DATA.rooms!.join[roomId].timeline.events.push(e as unknown as IRoomEvent);
} else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) { } else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) {
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e); NEXT_SYNC_DATA.rooms!.join[roomId].ephemeral.events.push(e as unknown as IMinimalEvent);
} else { } else {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e); NEXT_SYNC_DATA.rooms!.join[roomId].timeline.events.push(e as unknown as IRoomEvent);
} }
}); });
} }
@ -237,7 +246,7 @@ describe("MatrixClient room timelines", function() {
}); });
describe("paginated events", function() { describe("paginated events", function() {
let sbEvents; let sbEvents: Partial<IEvent>[];
const sbEndTok = "pagin_end"; const sbEndTok = "pagin_end";
beforeEach(function() { beforeEach(function() {
@ -559,7 +568,7 @@ describe("MatrixClient room timelines", function() {
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
]; ];
setNextSyncData(eventData); setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true; NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true;
return Promise.all([ return Promise.all([
httpBackend!.flush("/versions", 1), httpBackend!.flush("/versions", 1),
@ -593,7 +602,7 @@ describe("MatrixClient room timelines", function() {
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
]; ];
setNextSyncData(eventData); setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true; NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true;
return Promise.all([ return Promise.all([
httpBackend!.flush("/sync", 1), httpBackend!.flush("/sync", 1),
@ -638,7 +647,7 @@ describe("MatrixClient room timelines", function() {
end: "end_token", end: "end_token",
}; };
let room; let room: Room;
beforeEach(async () => { beforeEach(async () => {
setNextSyncData(initialSyncEventData); setNextSyncData(initialSyncEventData);

View File

@ -33,7 +33,7 @@ import {
IJoinedRoom, IJoinedRoom,
IStateEvent, IStateEvent,
IMinimalEvent, IMinimalEvent,
NotificationCountType, NotificationCountType, IEphemeral,
} from "../../src"; } from "../../src";
import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync'; import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync';
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
@ -524,105 +524,101 @@ describe("MatrixClient syncing", () => {
const syncData = { const syncData = {
rooms: { rooms: {
join: { join: {
[roomOne]: {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
},
[roomTwo]: {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName,
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
},
}, },
}, },
}; };
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
syncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName,
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
const nextSyncData = { const nextSyncData = {
rooms: { rooms: {
join: { join: {
[roomOne]: {
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" },
}),
],
},
},
[roomTwo]: {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText,
}),
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] },
}),
],
},
},
}, },
}, },
}; };
nextSyncData.rooms.join[roomOne] = {
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" },
}),
],
},
};
nextSyncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText,
}),
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] },
}),
],
},
};
it("should continually recalculate the right room name.", () => { it("should continually recalculate the right room name.", () => {
httpBackend!.when("GET", "/sync").respond(200, syncData); httpBackend!.when("GET", "/sync").respond(200, syncData);
httpBackend!.when("GET", "/sync").respond(200, nextSyncData); httpBackend!.when("GET", "/sync").respond(200, nextSyncData);
@ -635,9 +631,7 @@ describe("MatrixClient syncing", () => {
]).then(() => { ]).then(() => {
const room = client!.getRoom(roomOne)!; const room = client!.getRoom(roomOne)!;
// should have clobbered the name to the one from /events // should have clobbered the name to the one from /events
expect(room.name).toEqual( expect(room.name).toEqual(nextSyncData.rooms.join[roomOne].state.events[0].content?.name);
nextSyncData.rooms.join[roomOne].state.events[0].content.name,
);
}); });
}); });
@ -742,46 +736,48 @@ describe("MatrixClient syncing", () => {
const normalFirstSync = { const normalFirstSync = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
join: {}, join: {
}, [roomOne]: {
}; timeline: {
normalFirstSync.rooms.join[roomOne] = { events: [normalMessageEvent],
timeline: { prev_batch: "pagTok",
events: [normalMessageEvent], },
prev_batch: "pagTok", state: {
}, events: [roomCreateEvent],
state: { },
events: [roomCreateEvent], },
},
}, },
}; };
const nextSyncData = { const nextSyncData = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
join: {}, join: {
}, [roomOne]: {
}; timeline: {
nextSyncData.rooms.join[roomOne] = { events: [
timeline: { // In subsequent syncs, a marker event in timeline
events: [ // range should normally trigger
// In subsequent syncs, a marker event in timeline // `timelineNeedsRefresh=true` but this marker isn't
// range should normally trigger // being sent by the room creator so it has no
// `timelineNeedsRefresh=true` but this marker isn't // special meaning in existing room versions.
// being sent by the room creator so it has no utils.mkEvent({
// special meaning in existing room versions. type: UNSTABLE_MSC2716_MARKER.name,
utils.mkEvent({ room: roomOne,
type: UNSTABLE_MSC2716_MARKER.name, // The important part we're testing is here!
room: roomOne, // `userC` is not the room creator.
// The important part we're testing is here! user: userC,
// `userC` is not the room creator. skey: "",
user: userC, content: {
skey: "", "m.insertion_id": "$abc",
content: { },
"m.insertion_id": "$abc", }),
],
prev_batch: "pagTok",
}, },
}), },
], },
prev_batch: "pagTok",
}, },
}; };
@ -831,16 +827,17 @@ describe("MatrixClient syncing", () => {
const normalFirstSync = { const normalFirstSync = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
join: {}, join: {
}, [roomOne]: {
}; timeline: {
normalFirstSync.rooms.join[roomOne] = { events: [normalMessageEvent],
timeline: { prev_batch: "pagTok",
events: [normalMessageEvent], },
prev_batch: "pagTok", state: {
}, events: [roomCreateEvent],
state: { },
events: [roomCreateEvent], },
},
}, },
}; };
@ -849,16 +846,17 @@ describe("MatrixClient syncing", () => {
const syncData = { const syncData = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
join: {}, join: {
}, [roomOne]: {
}; timeline: {
syncData.rooms.join[roomOne] = { events: [normalMessageEvent],
timeline: { prev_batch: "pagTok",
events: [normalMessageEvent], },
prev_batch: "pagTok", state: {
}, events: [roomCreateEvent],
state: { },
events: [roomCreateEvent], },
},
}, },
}; };
@ -879,16 +877,17 @@ describe("MatrixClient syncing", () => {
const syncData = { const syncData = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
join: {}, join: {
}, [roomOne]: {
}; timeline: {
syncData.rooms.join[roomOne] = { events: [markerEventFromRoomCreator],
timeline: { prev_batch: "pagTok",
events: [markerEventFromRoomCreator], },
prev_batch: "pagTok", state: {
}, events: [roomCreateEvent],
state: { },
events: [roomCreateEvent], },
},
}, },
}; };
@ -909,19 +908,20 @@ describe("MatrixClient syncing", () => {
const syncData = { const syncData = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
join: {}, join: {
}, [roomOne]: {
}; timeline: {
syncData.rooms.join[roomOne] = { events: [normalMessageEvent],
timeline: { prev_batch: "pagTok",
events: [normalMessageEvent], },
prev_batch: "pagTok", state: {
}, events: [
state: { roomCreateEvent,
events: [ markerEventFromRoomCreator,
roomCreateEvent, ],
markerEventFromRoomCreator, },
], },
},
}, },
}; };
@ -942,17 +942,18 @@ describe("MatrixClient syncing", () => {
const nextSyncData = { const nextSyncData = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
join: {}, join: {
}, [roomOne]: {
}; timeline: {
nextSyncData.rooms.join[roomOne] = { events: [
timeline: { // In subsequent syncs, a marker event in timeline
events: [ // range should trigger `timelineNeedsRefresh=true`
// In subsequent syncs, a marker event in timeline markerEventFromRoomCreator,
// range should trigger `timelineNeedsRefresh=true` ],
markerEventFromRoomCreator, prev_batch: "pagTok",
], },
prev_batch: "pagTok", },
},
}, },
}; };
@ -993,24 +994,25 @@ describe("MatrixClient syncing", () => {
const nextSyncData = { const nextSyncData = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
join: {}, join: {
}, [roomOne]: {
}; timeline: {
nextSyncData.rooms.join[roomOne] = { events: [
timeline: { utils.mkMessage({
events: [ room: roomOne, user: otherUserId, msg: "hello again",
utils.mkMessage({ }),
room: roomOne, user: otherUserId, msg: "hello again", ],
}), prev_batch: "pagTok",
], },
prev_batch: "pagTok", state: {
}, events: [
state: { // In subsequent syncs, a marker event in state
events: [ // should trigger `timelineNeedsRefresh=true`
// In subsequent syncs, a marker event in state markerEventFromRoomCreator,
// should trigger `timelineNeedsRefresh=true` ],
markerEventFromRoomCreator, },
], },
},
}, },
}; };
@ -1095,19 +1097,20 @@ describe("MatrixClient syncing", () => {
const limitedSyncData = { const limitedSyncData = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
join: {}, join: {
}, [roomOne]: {
}; timeline: {
limitedSyncData.rooms.join[roomOne] = { events: [
timeline: { utils.mkMessage({
events: [ room: roomOne, user: otherUserId, msg: "world",
utils.mkMessage({ }),
room: roomOne, user: otherUserId, msg: "world", ],
}), // The important part, make the sync `limited`
], limited: true,
// The important part, make the sync `limited` prev_batch: "newerTok",
limited: true, },
prev_batch: "newerTok", },
},
}, },
}; };
httpBackend!.when("GET", "/sync").respond(200, limitedSyncData); httpBackend!.when("GET", "/sync").respond(200, limitedSyncData);
@ -1167,7 +1170,7 @@ describe("MatrixClient syncing", () => {
const eventsInRoom = syncData.rooms.join[roomOne].timeline.events; const eventsInRoom = syncData.rooms.join[roomOne].timeline.events;
const contextUrl = `/rooms/${encodeURIComponent(roomOne)}/context/` + const contextUrl = `/rooms/${encodeURIComponent(roomOne)}/context/` +
`${encodeURIComponent(eventsInRoom[0].event_id)}`; `${encodeURIComponent(eventsInRoom[0].event_id!)}`;
httpBackend!.when("GET", contextUrl) httpBackend!.when("GET", contextUrl)
.respond(200, () => { .respond(200, () => {
return { return {
@ -1202,17 +1205,18 @@ describe("MatrixClient syncing", () => {
const syncData = { const syncData = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
join: {}, join: {
}, [roomOne]: {
}; timeline: {
syncData.rooms.join[roomOne] = { events: [
timeline: { utils.mkMessage({
events: [ room: roomOne, user: otherUserId, msg: "hello",
utils.mkMessage({ }),
room: roomOne, user: otherUserId, msg: "hello", ],
}), prev_batch: "pagTok",
], },
prev_batch: "pagTok", },
},
}, },
}; };
@ -1229,17 +1233,18 @@ describe("MatrixClient syncing", () => {
const syncData = { const syncData = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
join: {}, join: {
}, [roomTwo]: {
}; timeline: {
syncData.rooms.join[roomTwo] = { events: [
timeline: { utils.mkMessage({
events: [ room: roomTwo, user: otherUserId, msg: "roomtwo",
utils.mkMessage({ }),
room: roomTwo, user: otherUserId, msg: "roomtwo", ],
}), prev_batch: "roomtwotok",
], },
prev_batch: "roomtwotok", },
},
}, },
}; };
@ -1261,18 +1266,19 @@ describe("MatrixClient syncing", () => {
const syncData = { const syncData = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
join: {}, join: {
}, [roomOne]: {
}; timeline: {
syncData.rooms.join[roomOne] = { events: [
timeline: { utils.mkMessage({
events: [ room: roomOne, user: otherUserId, msg: "world",
utils.mkMessage({ }),
room: roomOne, user: otherUserId, msg: "world", ],
}), limited: true,
], prev_batch: "newerTok",
limited: true, },
prev_batch: "newerTok", },
},
}, },
}; };
httpBackend!.when("GET", "/sync").respond(200, syncData); httpBackend!.when("GET", "/sync").respond(200, syncData);
@ -1304,44 +1310,46 @@ describe("MatrixClient syncing", () => {
const syncData = { const syncData = {
rooms: { rooms: {
join: { join: {
[roomOne]: {
ephemeral: {
events: [],
} as IEphemeral,
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
} as Partial<IJoinedRoom>,
},
}, },
}, },
}; };
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
beforeEach(() => { beforeEach(() => {
syncData.rooms.join[roomOne].ephemeral = { syncData.rooms.join[roomOne].ephemeral = {
@ -1351,16 +1359,15 @@ describe("MatrixClient syncing", () => {
it("should sync receipts from /sync.", () => { it("should sync receipts from /sync.", () => {
const ackEvent = syncData.rooms.join[roomOne].timeline.events[0]; const ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
const receipt = {}; const receipt: Record<string, any> = {};
receipt[ackEvent.event_id] = { receipt[ackEvent.event_id!] = {
"m.read": {}, "m.read": {},
}; };
receipt[ackEvent.event_id]["m.read"][userC] = { receipt[ackEvent.event_id!]["m.read"][userC] = {
ts: 176592842636, ts: 176592842636,
}; };
syncData.rooms.join[roomOne].ephemeral.events = [{ syncData.rooms.join[roomOne].ephemeral.events = [{
content: receipt, content: receipt,
room_id: roomOne,
type: "m.receipt", type: "m.receipt",
}]; }];
httpBackend!.when("GET", "/sync").respond(200, syncData); httpBackend!.when("GET", "/sync").respond(200, syncData);
@ -1425,7 +1432,7 @@ describe("MatrixClient syncing", () => {
}, },
}, },
}, },
}; } as unknown as ISyncResponse;
it("should sync unread notifications.", () => { it("should sync unread notifications.", () => {
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = { syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: { [THREAD_ID]: {
@ -1509,18 +1516,18 @@ describe("MatrixClient syncing", () => {
const syncData = { const syncData = {
next_batch: "batch_token", next_batch: "batch_token",
rooms: { rooms: {
leave: {}, leave: {
}, [roomTwo]: {
}; timeline: {
events: [
syncData.rooms.leave[roomTwo] = { utils.mkMessage({
timeline: { room: roomTwo, user: otherUserId, msg: "hello",
events: [ }),
utils.mkMessage({ ],
room: roomTwo, user: otherUserId, msg: "hello", prev_batch: "pagTok",
}), },
], },
prev_batch: "pagTok", },
}, },
}; };

View File

@ -126,12 +126,13 @@ describe("megolm key backups", function() {
const syncResponse = { const syncResponse = {
next_batch: 1, next_batch: 1,
rooms: { rooms: {
join: {}, join: {
}, [ROOM_ID]: {
}; timeline: {
syncResponse.rooms.join[ROOM_ID] = { events: [ENCRYPTED_EVENT],
timeline: { },
events: [ENCRYPTED_EVENT], },
},
}, },
}; };

View File

@ -119,13 +119,13 @@ describe("SlidingSyncSdk", () => {
}; };
// find an extension on a SlidingSyncSdk instance // find an extension on a SlidingSyncSdk instance
const findExtension = (name: string): Extension => { const findExtension = (name: string): Extension<any, any> => {
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled(); expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
const mockFn = mockSlidingSync!.registerExtension as jest.Mock; const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
// find the extension // find the extension
for (let i = 0; i < mockFn.mock.calls.length; i++) { for (let i = 0; i < mockFn.mock.calls.length; i++) {
const calledExtension = mockFn.mock.calls[i][0] as Extension; const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
if (calledExtension && calledExtension.name() === name) { if (calledExtension?.name() === name) {
return calledExtension; return calledExtension;
} }
} }
@ -581,7 +581,7 @@ describe("SlidingSyncSdk", () => {
}); });
describe("ExtensionE2EE", () => { describe("ExtensionE2EE", () => {
let ext: Extension; let ext: Extension<any, any>;
beforeAll(async () => { beforeAll(async () => {
await setupClient({ await setupClient({
@ -647,7 +647,7 @@ describe("SlidingSyncSdk", () => {
}); });
describe("ExtensionAccountData", () => { describe("ExtensionAccountData", () => {
let ext: Extension; let ext: Extension<any, any>;
beforeAll(async () => { beforeAll(async () => {
await setupClient(); await setupClient();
@ -773,7 +773,7 @@ describe("SlidingSyncSdk", () => {
}); });
describe("ExtensionToDevice", () => { describe("ExtensionToDevice", () => {
let ext: Extension; let ext: Extension<any, any>;
beforeAll(async () => { beforeAll(async () => {
await setupClient(); await setupClient();
@ -871,7 +871,7 @@ describe("SlidingSyncSdk", () => {
}); });
describe("ExtensionTyping", () => { describe("ExtensionTyping", () => {
let ext: Extension; let ext: Extension<any, any>;
beforeAll(async () => { beforeAll(async () => {
await setupClient(); await setupClient();
@ -970,7 +970,7 @@ describe("SlidingSyncSdk", () => {
}); });
describe("ExtensionReceipts", () => { describe("ExtensionReceipts", () => {
let ext: Extension; let ext: Extension<any, any>;
const generateReceiptResponse = ( const generateReceiptResponse = (
userId: string, roomId: string, eventId: string, recType: string, ts: number, userId: string, roomId: string, eventId: string, recType: string, ts: number,

View File

@ -18,7 +18,15 @@ limitations under the License.
import EventEmitter from "events"; import EventEmitter from "events";
import MockHttpBackend from "matrix-mock-request"; import MockHttpBackend from "matrix-mock-request";
import { SlidingSync, SlidingSyncState, ExtensionState, SlidingSyncEvent } from "../../src/sliding-sync"; import {
SlidingSync,
SlidingSyncState,
ExtensionState,
SlidingSyncEvent,
Extension,
SlidingSyncEventHandlerMap,
MSC3575RoomData,
} from "../../src/sliding-sync";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
import { logger } from "../../src/logger"; import { logger } from "../../src/logger";
import { MatrixClient } from "../../src"; import { MatrixClient } from "../../src";
@ -94,7 +102,7 @@ describe("SlidingSync", () => {
is_dm: true, is_dm: true,
}, },
}; };
const ext = { const ext: Extension<any, any> = {
name: () => "custom_extension", name: () => "custom_extension",
onRequest: (initial) => { return { initial: initial }; }, onRequest: (initial) => { return { initial: initial }; },
onResponse: (res) => { return {}; }, onResponse: (res) => { return {}; },
@ -107,7 +115,7 @@ describe("SlidingSync", () => {
slidingSync.start(); slidingSync.start();
// expect everything to be sent // expect everything to be sent
let txnId; let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
@ -390,8 +398,8 @@ describe("SlidingSync", () => {
}], }],
rooms: rooms, rooms: rooms,
}); });
const listenerData = {}; const listenerData: Record<string, MSC3575RoomData> = {};
const dataListener = (roomId, roomData) => { const dataListener: SlidingSyncEventHandlerMap[SlidingSyncEvent.RoomData] = (roomId, roomData) => {
expect(listenerData[roomId]).toBeFalsy(); expect(listenerData[roomId]).toBeFalsy();
listenerData[roomId] = roomData; listenerData[roomId] = roomData;
}; };
@ -912,7 +920,7 @@ describe("SlidingSync", () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1); slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
// modification before SlidingSync.start() // modification before SlidingSync.start()
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId])); const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
let txnId; let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
@ -944,7 +952,7 @@ describe("SlidingSync", () => {
ranges: [[0, 20]], ranges: [[0, 20]],
}; };
const promise = slidingSync.setList(0, newList); const promise = slidingSync.setList(0, newList);
let txnId; let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
@ -966,7 +974,7 @@ describe("SlidingSync", () => {
}); });
it("should resolve setListRanges during a connection", async () => { it("should resolve setListRanges during a connection", async () => {
const promise = slidingSync.setListRanges(0, [[20, 40]]); const promise = slidingSync.setListRanges(0, [[20, 40]]);
let txnId; let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
@ -992,7 +1000,7 @@ describe("SlidingSync", () => {
const promise = slidingSync.modifyRoomSubscriptionInfo({ const promise = slidingSync.modifyRoomSubscriptionInfo({
timeline_limit: 99, timeline_limit: 99,
}); });
let txnId; let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
@ -1016,7 +1024,7 @@ describe("SlidingSync", () => {
it("should reject earlier pending promises if a later transaction is acknowledged", async () => { it("should reject earlier pending promises if a later transaction is acknowledged", async () => {
// i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected. // i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected.
const gotTxnIds: any[] = []; const gotTxnIds: any[] = [];
const pushTxn = function(req) { const pushTxn = function(req: MockHttpBackend["requests"][0]) {
gotTxnIds.push(req.data.txn_id); gotTxnIds.push(req.data.txn_id);
}; };
const failPromise = slidingSync.setListRanges(0, [[20, 40]]); const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
@ -1032,7 +1040,7 @@ describe("SlidingSync", () => {
expect(failPromise2).rejects.toEqual(gotTxnIds[1]); expect(failPromise2).rejects.toEqual(gotTxnIds[1]);
const okPromise = slidingSync.setListRanges(0, [[0, 20]]); const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
let txnId; let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check((req) => { httpBackend!.when("POST", syncUrl).check((req) => {
txnId = req.data.txn_id; txnId = req.data.txn_id;
}).respond(200, () => { }).respond(200, () => {
@ -1050,7 +1058,7 @@ describe("SlidingSync", () => {
it("should not reject later pending promises if an earlier transaction is acknowledged", async () => { it("should not reject later pending promises if an earlier transaction is acknowledged", async () => {
// i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should. // i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should.
const gotTxnIds: any[] = []; const gotTxnIds: any[] = [];
const pushTxn = function(req) { const pushTxn = function(req: MockHttpBackend["requests"][0]) {
gotTxnIds.push(req.data?.txn_id); gotTxnIds.push(req.data?.txn_id);
}; };
const A = slidingSync.setListRanges(0, [[20, 40]]); const A = slidingSync.setListRanges(0, [[20, 40]]);
@ -1087,7 +1095,7 @@ describe("SlidingSync", () => {
promise.finally(() => { promise.finally(() => {
pending = false; pending = false;
}); });
let txnId; let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
@ -1275,21 +1283,21 @@ describe("SlidingSync", () => {
// Pre-extensions get called BEFORE processing the sync response // Pre-extensions get called BEFORE processing the sync response
const preExtName = "foobar"; const preExtName = "foobar";
let onPreExtensionRequest; let onPreExtensionRequest: Extension<any, any>["onRequest"];
let onPreExtensionResponse; let onPreExtensionResponse: Extension<any, any>["onResponse"];
// Post-extensions get called AFTER processing the sync response // Post-extensions get called AFTER processing the sync response
const postExtName = "foobar2"; const postExtName = "foobar2";
let onPostExtensionRequest; let onPostExtensionRequest: Extension<any, any>["onRequest"];
let onPostExtensionResponse; let onPostExtensionResponse: Extension<any, any>["onResponse"];
const extPre = { const extPre: Extension<any, any> = {
name: () => preExtName, name: () => preExtName,
onRequest: (initial) => { return onPreExtensionRequest(initial); }, onRequest: (initial) => { return onPreExtensionRequest(initial); },
onResponse: (res) => { return onPreExtensionResponse(res); }, onResponse: (res) => { return onPreExtensionResponse(res); },
when: () => ExtensionState.PreProcess, when: () => ExtensionState.PreProcess,
}; };
const extPost = { const extPost: Extension<any, any> = {
name: () => postExtName, name: () => postExtName,
onRequest: (initial) => { return onPostExtensionRequest(initial); }, onRequest: (initial) => { return onPostExtensionRequest(initial); },
onResponse: (res) => { return onPostExtensionResponse(res); }, onResponse: (res) => { return onPostExtensionResponse(res); },
@ -1421,7 +1429,7 @@ describe("SlidingSync", () => {
}); });
function timeout(delayMs: number, reason: string): { promise: Promise<never>, cancel: () => void } { function timeout(delayMs: number, reason: string): { promise: Promise<never>, cancel: () => void } {
let timeoutId; let timeoutId: NodeJS.Timeout;
return { return {
promise: new Promise((resolve, reject) => { promise: new Promise((resolve, reject) => {
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
@ -1454,7 +1462,7 @@ function listenUntil<T>(
const trace = new Error().stack?.split(`\n`)[2]; const trace = new Error().stack?.split(`\n`)[2];
const t = timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace); const t = timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace);
return Promise.race([new Promise<T>((resolve, reject) => { return Promise.race([new Promise<T>((resolve, reject) => {
const wrapper = (...args) => { const wrapper = (...args: any[]) => {
try { try {
const data = callback(...args); const data = callback(...args);
if (data) { if (data) {

View File

@ -332,7 +332,7 @@ export function mkReplyMessage(
* *
* @constructor * @constructor
*/ */
export class MockStorageApi { export class MockStorageApi implements Storage {
private data: Record<string, any> = {}; private data: Record<string, any> = {};
public get length() { public get length() {
@ -354,6 +354,10 @@ export class MockStorageApi {
public removeItem(k: string): void { public removeItem(k: string): void {
delete this.data[k]; delete this.data[k];
} }
public clear(): void {
this.data = {};
}
} }
/** /**

View File

@ -3,7 +3,7 @@ import '../olm-loader';
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import type { PkDecryption, PkSigning } from "@matrix-org/olm"; import type { PkDecryption, PkSigning } from "@matrix-org/olm";
import { MatrixClient } from "../../src/client"; import { IClaimOTKsResult, MatrixClient } from "../../src/client";
import { Crypto } from "../../src/crypto"; import { Crypto } from "../../src/crypto";
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store"; import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../MockStorageApi"; import { MockStorageApi } from "../MockStorageApi";
@ -23,16 +23,16 @@ import { IRoomEncryption, RoomList } from "../../src/crypto/RoomList";
const Olm = global.Olm; const Olm = global.Olm;
function awaitEvent(emitter, event) { function awaitEvent(emitter: EventEmitter, event: string): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve) => {
emitter.once(event, (result) => { emitter.once(event, (result) => {
resolve(result); resolve(result);
}); });
}); });
} }
async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent> { async function keyshareEventForEvent(client: MatrixClient, event: MatrixEvent, index?: number): Promise<MatrixEvent> {
const roomId = event.getRoomId(); const roomId = event.getRoomId()!;
const eventContent = event.getWireContent(); const eventContent = event.getWireContent();
const key = await client.crypto!.olmDevice.getInboundGroupSessionKey( const key = await client.crypto!.olmDevice.getInboundGroupSessionKey(
roomId, roomId,
@ -42,16 +42,16 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
); );
const ksEvent = new MatrixEvent({ const ksEvent = new MatrixEvent({
type: "m.forwarded_room_key", type: "m.forwarded_room_key",
sender: client.getUserId(), sender: client.getUserId()!,
content: { content: {
"algorithm": olmlib.MEGOLM_ALGORITHM, "algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": roomId, "room_id": roomId,
"sender_key": eventContent.sender_key, "sender_key": eventContent.sender_key,
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key, "sender_claimed_ed25519_key": key?.sender_claimed_ed25519_key,
"session_id": eventContent.session_id, "session_id": eventContent.session_id,
"session_key": key.key, "session_key": key?.key,
"chain_index": key.chain_index, "chain_index": key?.chain_index,
"forwarding_curve25519_key_chain": key.forwarding_curve_key_chain, "forwarding_curve25519_key_chain": key?.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": true, "org.matrix.msc3061.shared_history": true,
}, },
}); });
@ -172,7 +172,8 @@ describe("Crypto", function() {
}); });
describe('Session management', function() { describe('Session management', function() {
const otkResponse = { const otkResponse: IClaimOTKsResult = {
failures: {},
one_time_keys: { one_time_keys: {
'@alice:home.server': { '@alice:home.server': {
aliceDevice: { aliceDevice: {
@ -188,11 +189,12 @@ describe("Crypto", function() {
}, },
}, },
}; };
let crypto;
let mockBaseApis;
let mockRoomList;
let fakeEmitter; let crypto: Crypto;
let mockBaseApis: MatrixClient;
let mockRoomList: RoomList;
let fakeEmitter: EventEmitter;
beforeEach(async function() { beforeEach(async function() {
const mockStorage = new MockStorageApi() as unknown as Storage; const mockStorage = new MockStorageApi() as unknown as Storage;
@ -219,8 +221,8 @@ describe("Crypto", function() {
sendToDevice: jest.fn(), sendToDevice: jest.fn(),
getKeyBackupVersion: jest.fn(), getKeyBackupVersion: jest.fn(),
isGuest: jest.fn(), isGuest: jest.fn(),
}; } as unknown as MatrixClient;
mockRoomList = {}; mockRoomList = {} as unknown as RoomList;
fakeEmitter = new EventEmitter(); fakeEmitter = new EventEmitter();
@ -233,7 +235,7 @@ describe("Crypto", function() {
mockRoomList, mockRoomList,
[], [],
); );
crypto.registerEventHandlers(fakeEmitter); crypto.registerEventHandlers(fakeEmitter as any);
await crypto.init(); await crypto.init();
}); });
@ -245,7 +247,7 @@ describe("Crypto", function() {
const prom = new Promise<void>((resolve) => { const prom = new Promise<void>((resolve) => {
mockBaseApis.claimOneTimeKeys = function() { mockBaseApis.claimOneTimeKeys = function() {
resolve(); resolve();
return otkResponse; return Promise.resolve(otkResponse);
}; };
}); });
@ -989,7 +991,7 @@ describe("Crypto", function() {
ensureOlmSessionsForDevices.mockResolvedValue({}); ensureOlmSessionsForDevices.mockResolvedValue({});
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice"); encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
encryptMessageForDevice.mockImplementation(async (...[result,,,,,, payload]) => { encryptMessageForDevice.mockImplementation(async (...[result,,,,,, payload]) => {
result.plaintext = JSON.stringify(payload); result.plaintext = { type: 0, body: JSON.stringify(payload) };
}); });
client = new TestClient("@alice:example.org", "aliceweb"); client = new TestClient("@alice:example.org", "aliceweb");
@ -998,7 +1000,7 @@ describe("Crypto", function() {
encryptedPayload = { encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2", algorithm: "m.olm.v1.curve25519-aes-sha2",
sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key, sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key,
ciphertext: { plaintext: JSON.stringify(payload) }, ciphertext: { plaintext: { type: 0, body: JSON.stringify(payload) } },
}; };
}); });
@ -1046,7 +1048,7 @@ describe("Crypto", function() {
encryptMessageForDevice.mockImplementation(async (...[result,,,, userId, device, payload]) => { encryptMessageForDevice.mockImplementation(async (...[result,,,, userId, device, payload]) => {
// Refuse to encrypt to Carol's desktop device // Refuse to encrypt to Carol's desktop device
if (userId === "@carol:example.org" && device.deviceId === "caroldesktop") return; if (userId === "@carol:example.org" && device.deviceId === "caroldesktop") return;
result.plaintext = JSON.stringify(payload); result.plaintext = { type: 0, body: JSON.stringify(payload) };
}); });
client.httpBackend client.httpBackend

View File

@ -232,7 +232,7 @@ describe.each([
return store; return store;
}], }],
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) { ])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) {
let store; let store: IndexedDBCryptoStore;
beforeAll(() => { beforeAll(() => {
store = dbFactory(); store = dbFactory();

View File

@ -22,6 +22,7 @@ import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store
import { DeviceList } from "../../../src/crypto/DeviceList"; import { DeviceList } from "../../../src/crypto/DeviceList";
import { IDownloadKeyResult, MatrixClient } from "../../../src"; import { IDownloadKeyResult, MatrixClient } from "../../../src";
import { OlmDevice } from "../../../src/crypto/OlmDevice"; import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { CryptoStore } from "../../../src/crypto/store/base";
const signedDeviceList: IDownloadKeyResult = { const signedDeviceList: IDownloadKeyResult = {
"failures": {}, "failures": {},
@ -88,8 +89,8 @@ const signedDeviceList2: IDownloadKeyResult = {
}; };
describe('DeviceList', function() { describe('DeviceList', function() {
let downloadSpy; let downloadSpy: jest.Mock;
let cryptoStore; let cryptoStore: CryptoStore;
let deviceLists: DeviceList[] = []; let deviceLists: DeviceList[] = [];
beforeEach(function() { beforeEach(function() {
@ -112,7 +113,7 @@ describe('DeviceList', function() {
deviceId: 'HGKAWHRVJQ', deviceId: 'HGKAWHRVJQ',
} as unknown as MatrixClient; } as unknown as MatrixClient;
const mockOlm = { const mockOlm = {
verifySignature: function(key, message, signature) {}, verifySignature: function(key: string, message: string, signature: string) {},
} as unknown as OlmDevice; } as unknown as OlmDevice;
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize); const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
deviceLists.push(dl); deviceLists.push(dl);

View File

@ -17,6 +17,7 @@ limitations under the License.
import { mocked, MockedObject } from 'jest-mock'; import { mocked, MockedObject } from 'jest-mock';
import '../../../olm-loader'; import '../../../olm-loader';
import type { OutboundGroupSession } from "@matrix-org/olm";
import * as algorithms from "../../../../src/crypto/algorithms"; import * as algorithms from "../../../../src/crypto/algorithms";
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store"; import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
import * as testUtils from "../../../test-utils/test-utils"; import * as testUtils from "../../../test-utils/test-utils";
@ -31,6 +32,7 @@ import { TypedEventEmitter } from '../../../../src/models/typed-event-emitter';
import { ClientEvent, MatrixClient, RoomMember } from '../../../../src'; import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo'; import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning'; import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm";
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!; const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!; const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
@ -87,7 +89,7 @@ describe("MegolmDecryption", function() {
}); });
describe('receives some keys:', function() { describe('receives some keys:', function() {
let groupSession; let groupSession: OutboundGroupSession;
beforeEach(async function() { beforeEach(async function() {
groupSession = new global.Olm.OutboundGroupSession(); groupSession = new global.Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
@ -298,10 +300,10 @@ describe("MegolmDecryption", function() {
describe("session reuse and key reshares", () => { describe("session reuse and key reshares", () => {
const rotationPeriodMs = 999 * 24 * 60 * 60 * 1000; // 999 days, so we don't have to deal with it const rotationPeriodMs = 999 * 24 * 60 * 60 * 1000; // 999 days, so we don't have to deal with it
let megolmEncryption; let megolmEncryption: MegolmEncryptionClass;
let aliceDeviceInfo; let aliceDeviceInfo: DeviceInfo;
let mockRoom; let mockRoom: Room;
let olmDevice; let olmDevice: OlmDevice;
beforeEach(async () => { beforeEach(async () => {
// @ts-ignore assigning to readonly prop // @ts-ignore assigning to readonly prop
@ -342,7 +344,7 @@ describe("MegolmDecryption", function() {
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE', 'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
), ),
getFingerprint: jest.fn().mockReturnValue(''), getFingerprint: jest.fn().mockReturnValue(''),
}; } as unknown as DeviceInfo;
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({ mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
'@alice:home.server': { '@alice:home.server': {
@ -365,7 +367,7 @@ describe("MegolmDecryption", function() {
algorithm: 'm.megolm.v1.aes-sha2', algorithm: 'm.megolm.v1.aes-sha2',
rotation_period_ms: rotationPeriodMs, rotation_period_ms: rotationPeriodMs,
}, },
}); }) as MegolmEncryptionClass;
// Splice the real method onto the mock object as megolm uses this method // Splice the real method onto the mock object as megolm uses this method
// on the crypto class in order to encrypt / start sessions // on the crypto class in order to encrypt / start sessions
@ -381,7 +383,7 @@ describe("MegolmDecryption", function() {
[{ userId: "@alice:home.server" }], [{ userId: "@alice:home.server" }],
), ),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false), getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
}; } as unknown as Room;
}); });
it("should use larger otkTimeout when preparing to encrypt room", async () => { it("should use larger otkTimeout when preparing to encrypt room", async () => {
@ -397,11 +399,14 @@ describe("MegolmDecryption", function() {
}); });
it("should generate a new session if this one needs rotation", async () => { it("should generate a new session if this one needs rotation", async () => {
// @ts-ignore - private method access
const session = await megolmEncryption.prepareNewSession(false); const session = await megolmEncryption.prepareNewSession(false);
session.creationTime -= rotationPeriodMs + 10000; // a smidge over the rotation time session.creationTime -= rotationPeriodMs + 10000; // a smidge over the rotation time
// Inject expired session which needs rotation // Inject expired session which needs rotation
// @ts-ignore - private field access
megolmEncryption.setupPromise = Promise.resolve(session); megolmEncryption.setupPromise = Promise.resolve(session);
// @ts-ignore - private method access
const prepareNewSessionSpy = jest.spyOn(megolmEncryption, "prepareNewSession"); const prepareNewSessionSpy = jest.spyOn(megolmEncryption, "prepareNewSession");
await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", { await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text", body: "Some text",
@ -446,8 +451,8 @@ describe("MegolmDecryption", function() {
}); });
mockBaseApis.sendToDevice.mockClear(); mockBaseApis.sendToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice( await megolmEncryption.reshareKeyWithDevice!(
olmDevice.deviceCurve25519Key, olmDevice.deviceCurve25519Key!,
ct1.session_id, ct1.session_id,
'@alice:home.server', '@alice:home.server',
aliceDeviceInfo, aliceDeviceInfo,
@ -466,8 +471,8 @@ describe("MegolmDecryption", function() {
); );
mockBaseApis.queueToDevice.mockClear(); mockBaseApis.queueToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice( await megolmEncryption.reshareKeyWithDevice!(
olmDevice.deviceCurve25519Key, olmDevice.deviceCurve25519Key!,
ct1.session_id, ct1.session_id,
'@alice:home.server', '@alice:home.server',
aliceDeviceInfo, aliceDeviceInfo,

View File

@ -31,17 +31,21 @@ function makeOlmDevice() {
return olmDevice; return olmDevice;
} }
async function setupSession(initiator, opponent) { async function setupSession(initiator: OlmDevice, opponent: OlmDevice) {
await opponent.generateOneTimeKeys(1); await opponent.generateOneTimeKeys(1);
const keys = await opponent.getOneTimeKeys(); const keys = await opponent.getOneTimeKeys();
const firstKey = Object.values(keys['curve25519'])[0]; const firstKey = Object.values(keys['curve25519'])[0];
const sid = await initiator.createOutboundSession( const sid = await initiator.createOutboundSession(opponent.deviceCurve25519Key!, firstKey);
opponent.deviceCurve25519Key, firstKey,
);
return sid; return sid;
} }
function alwaysSucceed<T>(promise: Promise<T>): Promise<T | void> {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
describe("OlmDevice", function() { describe("OlmDevice", function() {
if (!global.Olm) { if (!global.Olm) {
logger.warn('Not running megolm unit tests: libolm not present'); logger.warn('Not running megolm unit tests: libolm not present');
@ -159,11 +163,6 @@ describe("OlmDevice", function() {
}, "ABCDEFG"), }, "ABCDEFG"),
], ],
}; };
function alwaysSucceed(promise) {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
// start two tasks that try to ensure that there's an olm session // start two tasks that try to ensure that there's an olm session
const promises = Promise.all([ const promises = Promise.all([
@ -235,12 +234,6 @@ describe("OlmDevice", function() {
], ],
}; };
function alwaysSucceed(promise) {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices( const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserAB, aliceOlmDevice, baseApis, devicesByUserAB,
)); ));

View File

@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MockedObject } from "jest-mock";
import '../../olm-loader'; import '../../olm-loader';
import { logger } from "../../../src/logger"; import { logger } from "../../../src/logger";
import * as olmlib from "../../../src/crypto/olmlib"; import * as olmlib from "../../../src/crypto/olmlib";
@ -30,7 +28,10 @@ import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils"; import { resetCrossSigningKeys } from "./crypto-utils";
import { BackupManager } from "../../../src/crypto/backup"; import { BackupManager } from "../../../src/crypto/backup";
import { StubStore } from "../../../src/store/stub"; import { StubStore } from "../../../src/store/stub";
import { MatrixScheduler } from '../../../src'; import { IndexedDBCryptoStore, MatrixScheduler } from '../../../src';
import { CryptoStore } from "../../../src/crypto/store/base";
import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm";
import { IKeyBackupInfo } from "../../../src/crypto/keybackup";
const Olm = global.Olm; const Olm = global.Olm;
@ -102,29 +103,36 @@ const CURVE25519_BACKUP_INFO = {
}, },
}; };
const AES256_BACKUP_INFO = { const AES256_BACKUP_INFO: IKeyBackupInfo = {
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2", algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: '1', version: '1',
auth_data: { auth_data: {} as IKeyBackupInfo["auth_data"],
// FIXME: add iv and mac
},
}; };
const keys = {}; const keys: Record<string, Uint8Array> = {};
function getCrossSigningKey(type) { function getCrossSigningKey(type: string) {
return keys[type]; return Promise.resolve(keys[type]);
} }
function saveCrossSigningKeys(k) { function saveCrossSigningKeys(k: Record<string, Uint8Array>) {
Object.assign(keys, k); Object.assign(keys, k);
} }
function makeTestClient(cryptoStore) { function makeTestScheduler(): MatrixScheduler {
const scheduler = [ return ([
"getQueueForEvent", "queueEvent", "removeEventFromQueue", "getQueueForEvent",
"queueEvent",
"removeEventFromQueue",
"setProcessFunction", "setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>; ] as const).reduce((r, k) => {
r[k] = jest.fn();
return r;
}, {} as MatrixScheduler);
}
function makeTestClient(cryptoStore: CryptoStore) {
const scheduler = makeTestScheduler();
const store = new StubStore(); const store = new StubStore();
return new MatrixClient({ return new MatrixClient({
@ -151,36 +159,33 @@ describe("MegolmBackup", function() {
return Olm.init(); return Olm.init();
}); });
let olmDevice; let olmDevice: OlmDevice;
let mockOlmLib; let mockOlmLib: typeof olmlib;
let mockCrypto; let mockCrypto: Crypto;
let cryptoStore; let cryptoStore: CryptoStore;
let megolmDecryption; let megolmDecryption: MegolmDecryptionClass;
beforeEach(async function() { beforeEach(async function() {
mockCrypto = testUtils.mock(Crypto, 'Crypto'); mockCrypto = testUtils.mock(Crypto, 'Crypto');
// @ts-ignore making mock
mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager"); mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager");
mockCrypto.backupKey = new Olm.PkEncryption(); mockCrypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
);
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
cryptoStore = new MemoryCryptoStore(); cryptoStore = new MemoryCryptoStore();
olmDevice = new OlmDevice(cryptoStore); olmDevice = new OlmDevice(cryptoStore);
// we stub out the olm encryption bits // we stub out the olm encryption bits
mockOlmLib = {}; mockOlmLib = {} as unknown as typeof olmlib;
mockOlmLib.ensureOlmSessionsForDevices = jest.fn(); mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
mockOlmLib.encryptMessageForDevice = mockOlmLib.encryptMessageForDevice =
jest.fn().mockResolvedValue(undefined); jest.fn().mockResolvedValue(undefined);
}); });
describe("backup", function() { describe("backup", function() {
let mockBaseApis; let mockBaseApis: MatrixClient;
beforeEach(function() { beforeEach(function() {
mockBaseApis = {}; mockBaseApis = {} as unknown as MatrixClient;
megolmDecryption = new MegolmDecryption({ megolmDecryption = new MegolmDecryption({
userId: '@user:id', userId: '@user:id',
@ -188,8 +193,9 @@ describe("MegolmBackup", function() {
olmDevice: olmDevice, olmDevice: olmDevice,
baseApis: mockBaseApis, baseApis: mockBaseApis,
roomId: ROOM_ID, roomId: ROOM_ID,
}); }) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib; megolmDecryption.olmlib = mockOlmLib;
// clobber the setTimeout function to run 100x faster. // clobber the setTimeout function to run 100x faster.
@ -239,6 +245,7 @@ describe("MegolmBackup", function() {
}; };
mockCrypto.cancelRoomKeyRequest = function() {}; mockCrypto.cancelRoomKeyRequest = function() {};
// @ts-ignore readonly field write
mockCrypto.backupManager = { mockCrypto.backupManager = {
backupGroupSession: jest.fn(), backupGroupSession: jest.fn(),
}; };
@ -264,21 +271,22 @@ describe("MegolmBackup", function() {
olmDevice: olmDevice, olmDevice: olmDevice,
baseApis: client, baseApis: client,
roomId: ROOM_ID, roomId: ROOM_ID,
}); }) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib; megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto() return client.initCrypto()
.then(() => { .then(() => {
return cryptoStore.doTxn( return cryptoStore.doTxn(
"readwrite", "readwrite",
[cryptoStore.STORE_SESSION], [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => { (txn) => {
cryptoStore.addEndToEndInboundGroupSession( cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(), groupSession.session_id(),
{ {
forwardingCurve25519KeyChain: undefined, forwardingCurve25519KeyChain: undefined!,
keysClaimed: { keysClaimed: {
ed25519: "SENDER_ED25519", ed25519: "SENDER_ED25519",
}, },
@ -298,25 +306,25 @@ describe("MegolmBackup", function() {
}); });
let numCalls = 0; let numCalls = 0;
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>( client.http.authedRequest = function(
method, path, queryParams, data, opts, method, path, queryParams, data, opts,
): Promise<T> { ): any {
++numCalls; ++numCalls;
expect(numCalls).toBeLessThanOrEqual(1); expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) { if (numCalls >= 2) {
// exit out of retry loop if there's something wrong // exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes")); reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({} as T); return Promise.resolve({});
} }
expect(method).toBe("PUT"); expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys"); expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1'); expect(queryParams?.version).toBe('1');
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined(); expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty( expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(), groupSession.session_id(),
); );
resolve(); resolve();
return Promise.resolve({} as T); return Promise.resolve({});
}; };
client.crypto!.backupManager.backupGroupSession( client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@ -343,8 +351,9 @@ describe("MegolmBackup", function() {
olmDevice: olmDevice, olmDevice: olmDevice,
baseApis: client, baseApis: client,
roomId: ROOM_ID, roomId: ROOM_ID,
}); }) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib; megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto() return client.initCrypto()
@ -354,13 +363,13 @@ describe("MegolmBackup", function() {
.then(() => { .then(() => {
return cryptoStore.doTxn( return cryptoStore.doTxn(
"readwrite", "readwrite",
[cryptoStore.STORE_SESSION], [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => { (txn) => {
cryptoStore.addEndToEndInboundGroupSession( cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(), groupSession.session_id(),
{ {
forwardingCurve25519KeyChain: undefined, forwardingCurve25519KeyChain: undefined!,
keysClaimed: { keysClaimed: {
ed25519: "SENDER_ED25519", ed25519: "SENDER_ED25519",
}, },
@ -381,25 +390,25 @@ describe("MegolmBackup", function() {
}); });
let numCalls = 0; let numCalls = 0;
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>( client.http.authedRequest = function(
method, path, queryParams, data, opts, method, path, queryParams, data, opts,
): Promise<T> { ): any {
++numCalls; ++numCalls;
expect(numCalls).toBeLessThanOrEqual(1); expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) { if (numCalls >= 2) {
// exit out of retry loop if there's something wrong // exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes")); reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({} as T); return Promise.resolve({});
} }
expect(method).toBe("PUT"); expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys"); expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1'); expect(queryParams?.version).toBe('1');
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined(); expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty( expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(), groupSession.session_id(),
); );
resolve(); resolve();
return Promise.resolve({} as T); return Promise.resolve({});
}; };
client.crypto!.backupManager.backupGroupSession( client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@ -426,8 +435,9 @@ describe("MegolmBackup", function() {
olmDevice: olmDevice, olmDevice: olmDevice,
baseApis: client, baseApis: client,
roomId: ROOM_ID, roomId: ROOM_ID,
}); }) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib; megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto(); await client.initCrypto();
@ -437,10 +447,10 @@ describe("MegolmBackup", function() {
let numCalls = 0; let numCalls = 0;
await Promise.all([ await Promise.all([
new Promise<void>((resolve, reject) => { new Promise<void>((resolve, reject) => {
let backupInfo; let backupInfo: Record<string, any> | BodyInit | undefined;
client.http.authedRequest = function( client.http.authedRequest = function(
method, path, queryParams, data, opts, method, path, queryParams, data, opts,
) { ): any {
++numCalls; ++numCalls;
expect(numCalls).toBeLessThanOrEqual(2); expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls === 1) { if (numCalls === 1) {
@ -486,10 +496,7 @@ describe("MegolmBackup", function() {
const ibGroupSession = new Olm.InboundGroupSession(); const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key()); ibGroupSession.create(groupSession.session_key());
const scheduler = [ const scheduler = makeTestScheduler();
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
const store = new StubStore(); const store = new StubStore();
const client = new MatrixClient({ const client = new MatrixClient({
baseUrl: "https://my.home.server", baseUrl: "https://my.home.server",
@ -509,20 +516,21 @@ describe("MegolmBackup", function() {
olmDevice: olmDevice, olmDevice: olmDevice,
baseApis: client, baseApis: client,
roomId: ROOM_ID, roomId: ROOM_ID,
}); }) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib; megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto(); await client.initCrypto();
await cryptoStore.doTxn( await cryptoStore.doTxn(
"readwrite", "readwrite",
[cryptoStore.STORE_SESSION], [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => { (txn) => {
cryptoStore.addEndToEndInboundGroupSession( cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(), groupSession.session_id(),
{ {
forwardingCurve25519KeyChain: undefined, forwardingCurve25519KeyChain: undefined!,
keysClaimed: { keysClaimed: {
ed25519: "SENDER_ED25519", ed25519: "SENDER_ED25519",
}, },
@ -542,26 +550,26 @@ describe("MegolmBackup", function() {
let numCalls = 0; let numCalls = 0;
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>( client.http.authedRequest = function(
method, path, queryParams, data, opts, method, path, queryParams, data, opts,
): Promise<T> { ): any {
++numCalls; ++numCalls;
expect(numCalls).toBeLessThanOrEqual(2); expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls >= 3) { if (numCalls >= 3) {
// exit out of retry loop if there's something wrong // exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes")); reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({} as T); return Promise.resolve({});
} }
expect(method).toBe("PUT"); expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys"); expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1'); expect(queryParams?.version).toBe('1');
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined(); expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty( expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(), groupSession.session_id(),
); );
if (numCalls > 1) { if (numCalls > 1) {
resolve(); resolve();
return Promise.resolve({} as T); return Promise.resolve({});
} else { } else {
return Promise.reject( return Promise.reject(
new Error("this is an expected failure"), new Error("this is an expected failure"),
@ -579,7 +587,7 @@ describe("MegolmBackup", function() {
}); });
describe("restore", function() { describe("restore", function() {
let client; let client: MatrixClient;
beforeEach(function() { beforeEach(function() {
client = makeTestClient(cryptoStore); client = makeTestClient(cryptoStore);
@ -590,8 +598,9 @@ describe("MegolmBackup", function() {
olmDevice: olmDevice, olmDevice: olmDevice,
baseApis: client, baseApis: client,
roomId: ROOM_ID, roomId: ROOM_ID,
}); }) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib; megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto(); return client.initCrypto();
@ -603,7 +612,7 @@ describe("MegolmBackup", function() {
it('can restore from backup (Curve25519 version)', function() { it('can restore from backup (Curve25519 version)', function() {
client.http.authedRequest = function() { client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
}; };
return client.restoreKeyBackupWithRecoveryKey( return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
@ -620,7 +629,7 @@ describe("MegolmBackup", function() {
it('can restore from backup (AES-256 version)', function() { it('can restore from backup (AES-256 version)', function() {
client.http.authedRequest = function() { client.http.authedRequest = function() {
return Promise.resolve(AES256_KEY_BACKUP_DATA); return Promise.resolve<any>(AES256_KEY_BACKUP_DATA);
}; };
return client.restoreKeyBackupWithRecoveryKey( return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
@ -637,7 +646,7 @@ describe("MegolmBackup", function() {
it('can restore backup by room (Curve25519 version)', function() { it('can restore backup by room (Curve25519 version)', function() {
client.http.authedRequest = function() { client.http.authedRequest = function() {
return Promise.resolve({ return Promise.resolve<any>({
rooms: { rooms: {
[ROOM_ID]: { [ROOM_ID]: {
sessions: { sessions: {
@ -649,7 +658,7 @@ describe("MegolmBackup", function() {
}; };
return client.restoreKeyBackupWithRecoveryKey( return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null, null, CURVE25519_BACKUP_INFO, null!, null!, CURVE25519_BACKUP_INFO,
).then(() => { ).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => { }).then((res) => {
@ -659,18 +668,18 @@ describe("MegolmBackup", function() {
it('has working cache functions', async function() { it('has working cache functions', async function() {
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]); const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client.crypto.storeSessionBackupPrivateKey(key); await client.crypto!.storeSessionBackupPrivateKey(key);
const result = await client.crypto.getSessionBackupPrivateKey(); const result = await client.crypto!.getSessionBackupPrivateKey();
expect(new Uint8Array(result)).toEqual(key); expect(new Uint8Array(result!)).toEqual(key);
}); });
it('caches session backup keys as it encounters them', async function() { it('caches session backup keys as it encounters them', async function() {
const cachedNull = await client.crypto.getSessionBackupPrivateKey(); const cachedNull = await client.crypto!.getSessionBackupPrivateKey();
expect(cachedNull).toBeNull(); expect(cachedNull).toBeNull();
client.http.authedRequest = function() { client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
}; };
await new Promise((resolve) => { await new Promise<void>((resolve) => {
client.restoreKeyBackupWithRecoveryKey( client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID, ROOM_ID,
@ -679,7 +688,7 @@ describe("MegolmBackup", function() {
{ cacheCompleteCallback: resolve }, { cacheCompleteCallback: resolve },
); );
}); });
const cachedKey = await client.crypto.getSessionBackupPrivateKey(); const cachedKey = await client.crypto!.getSessionBackupPrivateKey();
expect(cachedKey).not.toBeNull(); expect(cachedKey).not.toBeNull();
}); });
@ -688,7 +697,7 @@ describe("MegolmBackup", function() {
algorithm: "this.algorithm.does.not.exist", algorithm: "this.algorithm.does.not.exist",
}); });
client.http.authedRequest = function() { client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
}; };
await expect(client.restoreKeyBackupWithRecoveryKey( await expect(client.restoreKeyBackupWithRecoveryKey(
@ -702,10 +711,7 @@ describe("MegolmBackup", function() {
describe("flagAllGroupSessionsForBackup", () => { describe("flagAllGroupSessionsForBackup", () => {
it("should return number of sesions needing backup", async () => { it("should return number of sesions needing backup", async () => {
const scheduler = [ const scheduler = makeTestScheduler();
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
const store = new StubStore(); const store = new StubStore();
const client = new MatrixClient({ const client = new MatrixClient({
baseUrl: "https://my.home.server", baseUrl: "https://my.home.server",

View File

@ -18,23 +18,24 @@ limitations under the License.
import '../../olm-loader'; import '../../olm-loader';
import anotherjson from 'another-json'; import anotherjson from 'another-json';
import { PkSigning } from '@matrix-org/olm'; import { PkSigning } from '@matrix-org/olm';
import HttpBackend from "matrix-mock-request";
import * as olmlib from "../../../src/crypto/olmlib"; import * as olmlib from "../../../src/crypto/olmlib";
import { MatrixError } from '../../../src/http-api'; import { MatrixError } from '../../../src/http-api';
import { logger } from '../../../src/logger'; import { logger } from '../../../src/logger';
import { ICrossSigningKey, ICreateClientOpts, ISignedKey } from '../../../src/client'; import { ICrossSigningKey, ICreateClientOpts, ISignedKey } from '../../../src/client';
import { CryptoEvent } from '../../../src/crypto'; import { CryptoEvent, IBootstrapCrossSigningOpts } from '../../../src/crypto';
import { IDevice } from '../../../src/crypto/deviceinfo'; import { IDevice } from '../../../src/crypto/deviceinfo';
import { TestClient } from '../../TestClient'; import { TestClient } from '../../TestClient';
import { resetCrossSigningKeys } from "./crypto-utils"; import { resetCrossSigningKeys } from "./crypto-utils";
const PUSH_RULES_RESPONSE = { const PUSH_RULES_RESPONSE: Response = {
method: "GET", method: "GET",
path: "/pushrules/", path: "/pushrules/",
data: {}, data: {},
}; };
const filterResponse = function(userId) { const filterResponse = function(userId: string): Response {
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter"; const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
return { return {
method: "POST", method: "POST",
@ -43,7 +44,13 @@ const filterResponse = function(userId) {
}; };
}; };
function setHttpResponses(httpBackend, responses) { interface Response {
method: 'GET' | 'PUT' | 'POST' | 'DELETE';
path: string;
data: object;
}
function setHttpResponses(httpBackend: HttpBackend, responses: Response[]) {
responses.forEach(response => { responses.forEach(response => {
httpBackend httpBackend
.when(response.method, response.path) .when(response.method, response.path)
@ -54,13 +61,13 @@ function setHttpResponses(httpBackend, responses) {
async function makeTestClient( async function makeTestClient(
userInfo: { userId: string, deviceId: string}, userInfo: { userId: string, deviceId: string},
options: Partial<ICreateClientOpts> = {}, options: Partial<ICreateClientOpts> = {},
keys = {}, keys: Record<string, Uint8Array> = {},
) { ) {
function getCrossSigningKey(type) { function getCrossSigningKey(type: string) {
return keys[type]; return keys[type] ?? null;
} }
function saveCrossSigningKeys(k) { function saveCrossSigningKeys(k: Record<string, Uint8Array>) {
Object.assign(keys, k); Object.assign(keys, k);
} }
@ -142,7 +149,9 @@ describe("Cross Signing", function() {
alice.uploadKeySignatures = async () => ({ failures: {} }); alice.uploadKeySignatures = async () => ({ failures: {} });
alice.setAccountData = async () => ({}); alice.setAccountData = async () => ({});
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T | null> => ({} as T); alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T | null> => ({} as T);
const authUploadDeviceSigningKeys = async func => await func({}); const authUploadDeviceSigningKeys: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async func => {
await func({});
};
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass // Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
// through failure, stopping before actually applying changes. // through failure, stopping before actually applying changes.
@ -275,7 +284,7 @@ describe("Cross Signing", function() {
); );
// feed sync result that includes master key, ssk, device key // feed sync result that includes master key, ssk, device key
const responses = [ const responses: Response[] = [
PUSH_RULES_RESPONSE, PUSH_RULES_RESPONSE,
{ {
method: "POST", method: "POST",
@ -464,7 +473,7 @@ describe("Cross Signing", function() {
}); });
it.skip("should trust signatures received from other devices", async function() { it.skip("should trust signatures received from other devices", async function() {
const aliceKeys: Record<string, PkSigning> = {}; const aliceKeys: Record<string, Uint8Array> = {};
const { client: alice, httpBackend } = await makeTestClient( const { client: alice, httpBackend } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },
undefined, undefined,
@ -494,8 +503,7 @@ describe("Cross Signing", function() {
}); });
// @ts-ignore private property // @ts-ignore private property
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"] const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"].Osborne2;
.Osborne2;
const aliceDevice = { const aliceDevice = {
user_id: "@alice:example.com", user_id: "@alice:example.com",
device_id: "Osborne2", device_id: "Osborne2",
@ -549,7 +557,7 @@ describe("Cross Signing", function() {
// - ssk // - ssk
// - master key signed by her usk (pretend that it was signed by another // - master key signed by her usk (pretend that it was signed by another
// of Alice's devices) // of Alice's devices)
const responses = [ const responses: Response[] = [
PUSH_RULES_RESPONSE, PUSH_RULES_RESPONSE,
{ {
method: "POST", method: "POST",
@ -853,7 +861,7 @@ describe("Cross Signing", function() {
}); });
it("should offer to upgrade device verifications to cross-signing", async function() { it("should offer to upgrade device verifications to cross-signing", async function() {
let upgradeResolveFunc; let upgradeResolveFunc: Function;
const { client: alice } = await makeTestClient( const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },

View File

@ -1,14 +1,16 @@
import { IRecoveryKey } from '../../../src/crypto/api'; import { IRecoveryKey } from '../../../src/crypto/api';
import { CrossSigningLevel } from '../../../src/crypto/CrossSigning'; import { CrossSigningLevel } from '../../../src/crypto/CrossSigning';
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
import { MatrixClient } from "../../../src";
import { CryptoEvent } from "../../../src/crypto";
// needs to be phased out and replaced with bootstrapSecretStorage, // needs to be phased out and replaced with bootstrapSecretStorage,
// but that is doing too much extra stuff for it to be an easy transition. // but that is doing too much extra stuff for it to be an easy transition.
export async function resetCrossSigningKeys( export async function resetCrossSigningKeys(
client, client: MatrixClient,
{ level }: { level?: CrossSigningLevel} = {}, { level }: { level?: CrossSigningLevel} = {},
): Promise<void> { ): Promise<void> {
const crypto = client.crypto; const crypto = client.crypto!;
const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys); const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys);
try { try {
@ -28,7 +30,8 @@ export async function resetCrossSigningKeys(
crypto.crossSigningInfo.keys = oldKeys; crypto.crossSigningInfo.keys = oldKeys;
throw e; throw e;
} }
crypto.emit("crossSigning.keysChanged", {}); crypto.emit(CryptoEvent.KeysChanged, {});
// @ts-ignore
await crypto.afterCrossSigningLocalKeyChange(); await crypto.afterCrossSigningLocalKeyChange();
} }

View File

@ -16,6 +16,7 @@ limitations under the License.
import '../../olm-loader'; import '../../olm-loader';
import * as olmlib from "../../../src/crypto/olmlib"; import * as olmlib from "../../../src/crypto/olmlib";
import { IObject } from "../../../src/crypto/olmlib";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage"; import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage";
import { MatrixEvent } from "../../../src/models/event"; import { MatrixEvent } from "../../../src/models/event";
import { TestClient } from '../../TestClient'; import { TestClient } from '../../TestClient';
@ -23,9 +24,11 @@ import { makeTestClients } from './verification/util';
import { encryptAES } from "../../../src/crypto/aes"; import { encryptAES } from "../../../src/crypto/aes";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils"; import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from '../../../src/logger'; import { logger } from '../../../src/logger';
import { ClientEvent, ICreateClientOpts } from '../../../src/client'; import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from '../../../src/client';
import { ISecretStorageKeyInfo } from '../../../src/crypto/api'; import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
import { DeviceInfo } from '../../../src/crypto/deviceinfo'; import { DeviceInfo } from '../../../src/crypto/deviceinfo';
import { ISignatures } from "../../../src/@types/signed";
import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) { async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) {
const client = (new TestClient( const client = (new TestClient(
@ -48,9 +51,15 @@ async function makeTestClient(userInfo: { userId: string, deviceId: string}, opt
// Wrapper around pkSign to return a signed object. pkSign returns the // Wrapper around pkSign to return a signed object. pkSign returns the
// signature, rather than the signed object. // signature, rather than the signed object.
function sign(obj, key, userId) { function sign<T extends IObject | ICurve25519AuthData>(obj: T, key: Uint8Array, userId: string): T & {
signatures: ISignatures;
unsigned?: object;
} {
olmlib.pkSign(obj, key, userId, ''); olmlib.pkSign(obj, key, userId, '');
return obj; return obj as T & {
signatures: ISignatures;
unsigned?: object;
};
} }
describe("Secrets", function() { describe("Secrets", function() {
@ -169,12 +178,12 @@ describe("Secrets", function() {
return [newKeyId, key]; return [newKeyId, key];
}); });
let keys = {}; let keys: Record<string, Uint8Array> = {};
const alice = await makeTestClient( const alice = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },
{ {
cryptoCallbacks: { cryptoCallbacks: {
getCrossSigningKey: t => keys[t], getCrossSigningKey: t => Promise.resolve(keys[t]),
saveCrossSigningKeys: k => keys = k, saveCrossSigningKeys: k => keys = k,
getSecretStorageKey: getKey, getSecretStorageKey: getKey,
}, },
@ -227,7 +236,7 @@ describe("Secrets", function() {
cryptoCallbacks: { cryptoCallbacks: {
onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => { onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => {
expect(secretName).toBe("foo"); expect(secretName).toBe("foo");
return "bar"; return Promise.resolve("bar");
}, },
}, },
}, },
@ -354,7 +363,7 @@ describe("Secrets", function() {
const storagePublicKey = decryption.generate_key(); const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key(); const storagePrivateKey = decryption.get_private_key();
const bob = await makeTestClient( const bob: MatrixClient = await makeTestClient(
{ {
userId: "@bob:example.com", userId: "@bob:example.com",
deviceId: "bob1", deviceId: "bob1",
@ -364,15 +373,15 @@ describe("Secrets", function() {
getSecretStorageKey: async request => { getSecretStorageKey: async request => {
const defaultKeyId = await bob.getDefaultSecretStorageKeyId(); const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]); expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
return [defaultKeyId, storagePrivateKey]; return [defaultKeyId!, storagePrivateKey];
}, },
}, },
}, },
); );
bob.uploadDeviceSigningKeys = async () => {}; bob.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = async () => {}; bob.uploadKeySignatures = async () => ({ failures: {} });
bob.setAccountData = async function(eventType, contents, callback) { bob.setAccountData = async function(eventType, contents) {
const event = new MatrixEvent({ const event = new MatrixEvent({
type: eventType, type: eventType,
content: contents, content: contents,
@ -380,16 +389,19 @@ describe("Secrets", function() {
this.store.storeAccountDataEvents([ this.store.storeAccountDataEvents([
event, event,
]); ]);
this.emit("accountData", event); this.emit(ClientEvent.AccountData, event);
return {};
}; };
bob.crypto.backupManager.checkKeyBackup = async () => {}; bob.crypto!.backupManager.checkKeyBackup = async () => null;
const crossSigning = bob.crypto.crossSigningInfo; const crossSigning = bob.crypto!.crossSigningInfo;
const secretStorage = bob.crypto.secretStorage; const secretStorage = bob.crypto!.secretStorage;
// Set up cross-signing keys from scratch with specific storage key // Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapCrossSigning({ await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => await func({}), authUploadDeviceSigningKeys: async func => {
await func({});
},
}); });
await bob.bootstrapSecretStorage({ await bob.bootstrapSecretStorage({
createSecretStorageKey: async () => ({ createSecretStorageKey: async () => ({
@ -400,13 +412,15 @@ describe("Secrets", function() {
}); });
// Clear local cross-signing keys and read from secret storage // Clear local cross-signing keys and read from secret storage
bob.crypto.deviceList.storeCrossSigningForUser( bob.crypto!.deviceList.storeCrossSigningForUser(
"@bob:example.com", "@bob:example.com",
crossSigning.toStorage(), crossSigning.toStorage(),
); );
crossSigning.keys = {}; crossSigning.keys = {};
await bob.bootstrapCrossSigning({ await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => await func({}), authUploadDeviceSigningKeys: async func => {
await func({});
},
}); });
expect(crossSigning.getId()).toBeTruthy(); expect(crossSigning.getId()).toBeTruthy();
@ -422,7 +436,7 @@ describe("Secrets", function() {
user_signing: USK, user_signing: USK,
self_signing: SSK, self_signing: SSK,
}; };
const secretStorageKeys = { const secretStorageKeys: Record<string, Uint8Array> = {
key_id: SSSSKey, key_id: SSSSKey,
}; };
const alice = await makeTestClient( const alice = await makeTestClient(
@ -498,14 +512,14 @@ describe("Secrets", function() {
[`ed25519:${XSPubKey}`]: XSPubKey, [`ed25519:${XSPubKey}`]: XSPubKey,
}, },
}, },
self_signing: sign({ self_signing: sign<ICrossSigningKey>({
user_id: "@alice:example.com", user_id: "@alice:example.com",
usage: ["self_signing"], usage: ["self_signing"],
keys: { keys: {
[`ed25519:${SSPubKey}`]: SSPubKey, [`ed25519:${SSPubKey}`]: SSPubKey,
}, },
}, XSK, "@alice:example.com"), }, XSK, "@alice:example.com"),
user_signing: sign({ user_signing: sign<ICrossSigningKey>({
user_id: "@alice:example.com", user_id: "@alice:example.com",
usage: ["user_signing"], usage: ["user_signing"],
keys: { keys: {
@ -557,7 +571,7 @@ describe("Secrets", function() {
user_signing: USK, user_signing: USK,
self_signing: SSK, self_signing: SSK,
}; };
const secretStorageKeys = { const secretStorageKeys: Record<string, Uint8Array> = {
key_id: SSSSKey, key_id: SSSSKey,
}; };
const alice = await makeTestClient( const alice = await makeTestClient(
@ -642,14 +656,14 @@ describe("Secrets", function() {
[`ed25519:${XSPubKey}`]: XSPubKey, [`ed25519:${XSPubKey}`]: XSPubKey,
}, },
}, },
self_signing: sign({ self_signing: sign<ICrossSigningKey>({
user_id: "@alice:example.com", user_id: "@alice:example.com",
usage: ["self_signing"], usage: ["self_signing"],
keys: { keys: {
[`ed25519:${SSPubKey}`]: SSPubKey, [`ed25519:${SSPubKey}`]: SSPubKey,
}, },
}, XSK, "@alice:example.com"), }, XSK, "@alice:example.com"),
user_signing: sign({ user_signing: sign<ICrossSigningKey>({
user_id: "@alice:example.com", user_id: "@alice:example.com",
usage: ["user_signing"], usage: ["user_signing"],
keys: { keys: {

View File

@ -18,12 +18,12 @@ import "../../../olm-loader";
import { makeTestClients } from './util'; import { makeTestClients } from './util';
import { MatrixEvent } from "../../../../src/models/event"; import { MatrixEvent } from "../../../../src/models/event";
import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS"; import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo"; import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo";
import { CryptoEvent, verificationMethods } from "../../../../src/crypto"; import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import * as olmlib from "../../../../src/crypto/olmlib"; import * as olmlib from "../../../../src/crypto/olmlib";
import { logger } from "../../../../src/logger"; import { logger } from "../../../../src/logger";
import { resetCrossSigningKeys } from "../crypto-utils"; import { resetCrossSigningKeys } from "../crypto-utils";
import { VerificationBase as Verification, VerificationBase } from "../../../../src/crypto/verification/Base"; import { VerificationBase } from "../../../../src/crypto/verification/Base";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel"; import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { MatrixClient } from "../../../../src"; import { MatrixClient } from "../../../../src";
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest"; import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
@ -31,8 +31,8 @@ import { TestClient } from "../../../TestClient";
const Olm = global.Olm; const Olm = global.Olm;
let ALICE_DEVICES; let ALICE_DEVICES: Record<string, IDevice>;
let BOB_DEVICES; let BOB_DEVICES: Record<string, IDevice>;
describe("SAS verification", function() { describe("SAS verification", function() {
if (!global.Olm) { if (!global.Olm) {
@ -75,7 +75,7 @@ describe("SAS verification", function() {
let bob: TestClient; let bob: TestClient;
let aliceSasEvent: ISasEvent | null; let aliceSasEvent: ISasEvent | null;
let bobSasEvent: ISasEvent | null; let bobSasEvent: ISasEvent | null;
let aliceVerifier: Verification<any, any>; let aliceVerifier: SAS;
let bobPromise: Promise<VerificationBase<any, any>>; let bobPromise: Promise<VerificationBase<any, any>>;
let clearTestClientTimeouts: () => void; let clearTestClientTimeouts: () => void;
@ -95,25 +95,25 @@ describe("SAS verification", function() {
ALICE_DEVICES = { ALICE_DEVICES = {
Osborne2: { Osborne2: {
user_id: "@alice:example.com",
device_id: "Osborne2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: { keys: {
"ed25519:Osborne2": aliceDevice.deviceEd25519Key, "ed25519:Osborne2": aliceDevice.deviceEd25519Key!,
"curve25519:Osborne2": aliceDevice.deviceCurve25519Key, "curve25519:Osborne2": aliceDevice.deviceCurve25519Key!,
}, },
verified: DeviceInfo.DeviceVerification.UNVERIFIED,
known: false,
}, },
}; };
BOB_DEVICES = { BOB_DEVICES = {
Dynabook: { Dynabook: {
user_id: "@bob:example.com",
device_id: "Dynabook",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: { keys: {
"ed25519:Dynabook": bobDevice.deviceEd25519Key, "ed25519:Dynabook": bobDevice.deviceEd25519Key!,
"curve25519:Dynabook": bobDevice.deviceCurve25519Key, "curve25519:Dynabook": bobDevice.deviceCurve25519Key!,
}, },
verified: DeviceInfo.DeviceVerification.UNVERIFIED,
known: false,
}, },
}; };
@ -136,7 +136,7 @@ describe("SAS verification", function() {
bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => { bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => { bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier!.on("show_sas", (e) => { (<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) { if (!e.sas.emoji || !e.sas.decimal) {
e.cancel(); e.cancel();
} else if (!aliceSasEvent) { } else if (!aliceSasEvent) {
@ -158,7 +158,7 @@ describe("SAS verification", function() {
aliceVerifier = alice.client.beginKeyVerification( aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!, verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!,
); ) as SAS;
aliceVerifier.on(SasEvent.ShowSas, (e) => { aliceVerifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) { if (!e.sas.emoji || !e.sas.decimal) {
e.cancel(); e.cancel();
@ -413,7 +413,7 @@ describe("SAS verification", function() {
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => { const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => { bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier!.on("show_sas", (e) => { (<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => {
e.mismatch(); e.mismatch();
}); });
resolve(request.verifier!); resolve(request.verifier!);
@ -443,13 +443,13 @@ describe("SAS verification", function() {
}); });
describe("verification in DM", function() { describe("verification in DM", function() {
let alice; let alice: TestClient;
let bob; let bob: TestClient;
let aliceSasEvent; let aliceSasEvent: ISasEvent | null;
let bobSasEvent; let bobSasEvent: ISasEvent | null;
let aliceVerifier; let aliceVerifier: SAS;
let bobPromise; let bobPromise: Promise<void>;
let clearTestClientTimeouts; let clearTestClientTimeouts: Function;
beforeEach(async function() { beforeEach(async function() {
[[alice, bob], clearTestClientTimeouts] = await makeTestClients( [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
@ -477,7 +477,7 @@ describe("SAS verification", function() {
); );
}; };
alice.client.downloadKeys = () => { alice.client.downloadKeys = () => {
return Promise.resolve(); return Promise.resolve({});
}; };
bob.client.crypto!.setDeviceVerification = jest.fn(); bob.client.crypto!.setDeviceVerification = jest.fn();
@ -495,16 +495,16 @@ describe("SAS verification", function() {
return "bob+base64+ed25519+key"; return "bob+base64+ed25519+key";
}; };
bob.client.downloadKeys = () => { bob.client.downloadKeys = () => {
return Promise.resolve(); return Promise.resolve({});
}; };
aliceSasEvent = null; aliceSasEvent = null;
bobSasEvent = null; bobSasEvent = null;
bobPromise = new Promise<void>((resolve, reject) => { bobPromise = new Promise<void>((resolve, reject) => {
bob.client.on("crypto.verification.request", async (request) => { bob.client.on(CryptoEvent.VerificationRequest, async (request) => {
const verifier = request.beginKeyVerification(SAS.NAME); const verifier = request.beginKeyVerification(SAS.NAME) as SAS;
verifier.on("show_sas", (e) => { verifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) { if (!e.sas.emoji || !e.sas.decimal) {
e.cancel(); e.cancel();
} else if (!aliceSasEvent) { } else if (!aliceSasEvent) {
@ -525,12 +525,10 @@ describe("SAS verification", function() {
}); });
}); });
const aliceRequest = await alice.client.requestVerificationDM( const aliceRequest = await alice.client.requestVerificationDM(bob.client.getUserId()!, "!room_id");
bob.client.getUserId(), "!room_id",
);
await aliceRequest.waitFor(r => r.started); await aliceRequest.waitFor(r => r.started);
aliceVerifier = aliceRequest.verifier; aliceVerifier = aliceRequest.verifier! as SAS;
aliceVerifier.on("show_sas", (e) => { aliceVerifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) { if (!e.sas.emoji || !e.sas.decimal) {
e.cancel(); e.cancel();
} else if (!bobSasEvent) { } else if (!bobSasEvent) {

View File

@ -125,7 +125,7 @@ describe("self-verifications", () => {
expect(restoreKeyBackupWithCache).toHaveBeenCalled(); expect(restoreKeyBackupWithCache).toHaveBeenCalled();
expect(result).toBeInstanceOf(Array); expect(result).toBeInstanceOf(Array);
expect(result[0][0]).toBe(testKeyPub); expect(result![0][0]).toBe(testKeyPub);
expect(result[1][0]).toBe(testKeyPub); expect(result![1][0]).toBe(testKeyPub);
}); });
}); });

View File

@ -16,13 +16,21 @@ limitations under the License.
*/ */
import { TestClient } from '../../../TestClient'; import { TestClient } from '../../../TestClient';
import { MatrixEvent } from "../../../../src/models/event"; import { IContent, MatrixEvent } from "../../../../src/models/event";
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set"; import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
import { Room, RoomEvent } from "../../../../src/models/room"; import { Room, RoomEvent } from "../../../../src/models/room";
import { logger } from '../../../../src/logger'; import { logger } from '../../../../src/logger';
import { MatrixClient, ClientEvent } from '../../../../src/client'; import { MatrixClient, ClientEvent, ICreateClientOpts } from '../../../../src/client';
export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> { interface UserInfo {
userId: string;
deviceId: string;
}
export async function makeTestClients(
userInfos: UserInfo[],
options: Partial<ICreateClientOpts>,
): Promise<[TestClient[], () => void]> {
const clients: TestClient[] = []; const clients: TestClient[] = [];
const timeouts: ReturnType<typeof setTimeout>[] = []; const timeouts: ReturnType<typeof setTimeout>[] = [];
const clientMap: Record<string, Record<string, MatrixClient>> = {}; const clientMap: Record<string, Record<string, MatrixClient>> = {};
@ -51,7 +59,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
} }
return {}; return {};
}; };
const makeSendEvent = (matrixClient: MatrixClient) => (room, type, content) => { const makeSendEvent = (matrixClient: MatrixClient) => (room: string, type: string, content: IContent) => {
// make up a unique ID as the event ID // make up a unique ID as the event ID
const eventId = "$" + matrixClient.makeTxnId(); const eventId = "$" + matrixClient.makeTxnId();
const rawEvent = { const rawEvent = {
@ -88,11 +96,12 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
}; };
for (const userInfo of userInfos) { for (const userInfo of userInfos) {
let keys = {}; let keys: Record<string, Uint8Array> = {};
if (!options) options = {}; if (!options) options = {};
if (!options.cryptoCallbacks) options.cryptoCallbacks = {}; if (!options.cryptoCallbacks) options.cryptoCallbacks = {};
if (!options.cryptoCallbacks.saveCrossSigningKeys) { if (!options.cryptoCallbacks.saveCrossSigningKeys) {
options.cryptoCallbacks.saveCrossSigningKeys = k => { keys = k; }; options.cryptoCallbacks.saveCrossSigningKeys = k => { keys = k; };
// @ts-ignore tsc getting confused by overloads
options.cryptoCallbacks.getCrossSigningKey = typ => keys[typ]; options.cryptoCallbacks.getCrossSigningKey = typ => keys[typ];
} }
const testClient = new TestClient( const testClient = new TestClient(
@ -104,6 +113,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
} }
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client; clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
testClient.client.sendToDevice = makeSendToDevice(testClient.client); testClient.client.sendToDevice = makeSendToDevice(testClient.client);
// @ts-ignore tsc getting confused by overloads
testClient.client.sendEvent = makeSendEvent(testClient.client); testClient.client.sendEvent = makeSendEvent(testClient.client);
clients.push(testClient); clients.push(testClient);
} }

View File

@ -18,7 +18,7 @@ import { VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE } from
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel"; import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
import { ToDeviceChannel } from import { ToDeviceChannel } from
"../../../../src/crypto/verification/request/ToDeviceChannel"; "../../../../src/crypto/verification/request/ToDeviceChannel";
import { MatrixEvent } from "../../../../src/models/event"; import { IContent, MatrixEvent } from "../../../../src/models/event";
import { MatrixClient } from "../../../../src/client"; import { MatrixClient } from "../../../../src/client";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel"; import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { VerificationBase } from "../../../../src/crypto/verification/Base"; import { VerificationBase } from "../../../../src/crypto/verification/Base";
@ -30,12 +30,12 @@ type MockClient = MatrixClient & {
function makeMockClient(userId: string, deviceId: string): MockClient { function makeMockClient(userId: string, deviceId: string): MockClient {
let counter = 1; let counter = 1;
let events: MatrixEvent[] = []; let events: MatrixEvent[] = [];
const deviceEvents = {}; const deviceEvents: Record<string, Record<string, MatrixEvent[]>> = {};
return { return {
getUserId() { return userId; }, getUserId() { return userId; },
getDeviceId() { return deviceId; }, getDeviceId() { return deviceId; },
sendEvent(roomId, type, content) { sendEvent(roomId: string, type: string, content: IContent) {
counter = counter + 1; counter = counter + 1;
const eventId = `$${userId}-${deviceId}-${counter}`; const eventId = `$${userId}-${deviceId}-${counter}`;
events.push(new MatrixEvent({ events.push(new MatrixEvent({
@ -49,7 +49,7 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
return Promise.resolve({ event_id: eventId }); return Promise.resolve({ event_id: eventId });
}, },
sendToDevice(type, msgMap) { sendToDevice(type: string, msgMap: Record<string, Record<string, IContent>>) {
for (const userId of Object.keys(msgMap)) { for (const userId of Object.keys(msgMap)) {
const deviceMap = msgMap[userId]; const deviceMap = msgMap[userId];
for (const deviceId of Object.keys(deviceMap)) { for (const deviceId of Object.keys(deviceMap)) {
@ -111,7 +111,7 @@ class MockVerifier extends VerificationBase<'', any> {
} }
} }
async handleEvent(event) { async handleEvent(event: MatrixEvent) {
if (event.getType() === DONE_TYPE && !this._startEvent) { if (event.getType() === DONE_TYPE && !this._startEvent) {
await this._channel.send(DONE_TYPE, {}); await this._channel.send(DONE_TYPE, {});
} }
@ -122,7 +122,7 @@ class MockVerifier extends VerificationBase<'', any> {
} }
} }
function makeRemoteEcho(event) { function makeRemoteEcho(event: MatrixEvent) {
return new MatrixEvent(Object.assign({}, event.event, { return new MatrixEvent(Object.assign({}, event.event, {
unsigned: { unsigned: {
transaction_id: "abc", transaction_id: "abc",

View File

@ -17,7 +17,7 @@ limitations under the License.
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { logger } from "../../src/logger"; import { logger } from "../../src/logger";
import { MatrixClient, ClientEvent } from "../../src/client"; import { ClientEvent, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
import { Filter } from "../../src/filter"; import { Filter } from "../../src/filter";
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
import { import {
@ -36,7 +36,16 @@ import { ReceiptType } from "../../src/@types/read_receipts";
import * as testUtils from "../test-utils/test-utils"; import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers"; import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon"; import { M_BEACON_INFO } from "../../src/@types/beacon";
import { ContentHelpers, EventTimeline, MatrixError, Room } from "../../src"; import {
ContentHelpers,
EventTimeline, ICreateRoomOpts,
IRequestOpts,
MatrixError,
MatrixHttpApi,
MatrixScheduler,
Method,
Room,
} from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call"; import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon"; import { makeBeaconEvent } from "../test-utils/beacon";
import { import {
@ -44,6 +53,9 @@ import {
POLICIES_ACCOUNT_EVENT_TYPE, POLICIES_ACCOUNT_EVENT_TYPE,
PolicyScope, PolicyScope,
} from "../../src/models/invites-ignorer"; } from "../../src/models/invites-ignorer";
import { IOlmDevice } from "../../src/crypto/algorithms/megolm";
import { QueryDict } from "../../src/utils";
import { SyncState } from "../../src/sync";
jest.useFakeTimers(); jest.useFakeTimers();
@ -52,17 +64,36 @@ jest.mock("../../src/webrtc/call", () => ({
supportsMatrixCall: jest.fn(() => false), supportsMatrixCall: jest.fn(() => false),
})); }));
type HttpLookup = {
method: string;
path: string;
data?: Record<string, any>;
error?: object;
expectBody?: Record<string, any>;
expectQueryParams?: QueryDict;
thenCall?: Function;
};
interface Options extends ICreateRoomOpts {
_roomId?: string;
}
type WrappedRoom = Room & {
_options: Options;
_state: Map<string, any>;
};
describe("MatrixClient", function() { describe("MatrixClient", function() {
const userId = "@alice:bar"; const userId = "@alice:bar";
const identityServerUrl = "https://identity.server"; const identityServerUrl = "https://identity.server";
const identityServerDomain = "identity.server"; const identityServerDomain = "identity.server";
let client; let client: MatrixClient;
let store; let store: Store;
let scheduler; let scheduler: MatrixScheduler;
const KEEP_ALIVE_PATH = "/_matrix/client/versions"; const KEEP_ALIVE_PATH = "/_matrix/client/versions";
const PUSH_RULES_RESPONSE = { const PUSH_RULES_RESPONSE: HttpLookup = {
method: "GET", method: "GET",
path: "/pushrules/", path: "/pushrules/",
data: {}, data: {},
@ -70,7 +101,7 @@ describe("MatrixClient", function() {
const FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter"; const FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
const FILTER_RESPONSE = { const FILTER_RESPONSE: HttpLookup = {
method: "POST", method: "POST",
path: FILTER_PATH, path: FILTER_PATH,
data: { filter_id: "f1lt3r" }, data: { filter_id: "f1lt3r" },
@ -82,29 +113,21 @@ describe("MatrixClient", function() {
rooms: {}, rooms: {},
}; };
const SYNC_RESPONSE = { const SYNC_RESPONSE: HttpLookup = {
method: "GET", method: "GET",
path: "/sync", path: "/sync",
data: SYNC_DATA, data: SYNC_DATA,
}; };
// items are popped off when processed and block if no items left. // items are popped off when processed and block if no items left.
let httpLookups: { let httpLookups: HttpLookup[] = [];
method: string;
path: string;
data?: object;
error?: object;
expectBody?: object;
expectQueryParams?: object;
thenCall?: Function;
}[] = [];
let acceptKeepalives: boolean; let acceptKeepalives: boolean;
let pendingLookup: { let pendingLookup: {
promise: Promise<any>; promise: Promise<any>;
method: string; method: string;
path: string; path: string;
} | null = null; } | null = null;
function httpReq(method, path, qp, data, prefix) { function httpReq(method: Method, path: string, qp?: QueryDict, data?: BodyInit, opts?: IRequestOpts) {
if (path === KEEP_ALIVE_PATH && acceptKeepalives) { if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
return Promise.resolve({ return Promise.resolve({
unstable_features: { unstable_features: {
@ -145,7 +168,7 @@ describe("MatrixClient", function() {
} }
if (next.expectQueryParams) { if (next.expectQueryParams) {
Object.keys(next.expectQueryParams).forEach(function(k) { Object.keys(next.expectQueryParams).forEach(function(k) {
expect(qp[k]).toEqual(next.expectQueryParams![k]); expect(qp?.[k]).toEqual(next.expectQueryParams![k]);
}); });
} }
@ -184,24 +207,38 @@ describe("MatrixClient", function() {
userId: userId, userId: userId,
}); });
// FIXME: We shouldn't be yanking http like this. // FIXME: We shouldn't be yanking http like this.
client.http = [ client.http = ([
"authedRequest", "getContentUri", "request", "uploadContent", "authedRequest",
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); "getContentUri",
client.http.authedRequest.mockImplementation(httpReq); "request",
client.http.request.mockImplementation(httpReq); "uploadContent",
] as const).reduce((r, k) => {
r[k] = jest.fn();
return r;
}, {} as MatrixHttpApi<any>);
mocked(client.http.authedRequest).mockImplementation(httpReq);
mocked(client.http.request).mockImplementation(httpReq);
} }
beforeEach(function() { beforeEach(function() {
scheduler = [ scheduler = ([
"getQueueForEvent", "queueEvent", "removeEventFromQueue", "getQueueForEvent",
"queueEvent",
"removeEventFromQueue",
"setProcessFunction", "setProcessFunction",
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); ] as const).reduce((r, k) => {
store = [ r[k] = jest.fn();
return r;
}, {} as MatrixScheduler);
store = ([
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser", "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter", "getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
"getSyncAccumulator", "startup", "deleteAllData", "startup", "deleteAllData",
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); ] as const).reduce((r, k) => {
r[k] = jest.fn();
return r;
}, {} as Store);
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null)); store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null)); store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null)); store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
@ -225,7 +262,7 @@ describe("MatrixClient", function() {
// means they may call /events and then fail an expect() which will fail // means they may call /events and then fail an expect() which will fail
// a DIFFERENT test (pollution between tests!) - we return unresolved // a DIFFERENT test (pollution between tests!) - we return unresolved
// promises to stop the client from continuing to run. // promises to stop the client from continuing to run.
client.http.authedRequest.mockImplementation(function() { mocked(client.http.authedRequest).mockImplementation(function() {
return new Promise(() => {}); return new Promise(() => {});
}); });
client.stopClient(); client.stopClient();
@ -289,7 +326,7 @@ describe("MatrixClient", function() {
const txnId = client.makeTxnId(); const txnId = client.makeTxnId();
const room = new Room(roomId, client, userId); const room = new Room(roomId, client, userId);
store.getRoom.mockReturnValue(room); mocked(store.getRoom).mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId }); const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false); room.createThread(threadId, rootEvent, [rootEvent], false);
@ -329,7 +366,7 @@ describe("MatrixClient", function() {
}; };
const room = new Room(roomId, client, userId); const room = new Room(roomId, client, userId);
store.getRoom.mockReturnValue(room); mocked(store.getRoom).mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId }); const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false); room.createThread(threadId, rootEvent, [rootEvent], false);
@ -359,7 +396,7 @@ describe("MatrixClient", function() {
const userId = "@test:example.org"; const userId = "@test:example.org";
const roomId = "!room:example.org"; const roomId = "!room:example.org";
const roomName = "Test Tree"; const roomName = "Test Tree";
const mockRoom = {}; const mockRoom = {} as unknown as Room;
const fn = jest.fn().mockImplementation((opts) => { const fn = jest.fn().mockImplementation((opts) => {
expect(opts).toMatchObject({ expect(opts).toMatchObject({
name: roomName, name: roomName,
@ -431,23 +468,23 @@ describe("MatrixClient", function() {
throw new Error("Unexpected event type or state key"); throw new Error("Unexpected event type or state key");
} }
}, },
}, } as Room["currentState"],
}; } as unknown as Room;
client.getRoom = (getRoomId) => { client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId); expect(getRoomId).toEqual(roomId);
return mockRoom; return mockRoom;
}; };
const tree = client.unstableGetFileTreeSpace(roomId); const tree = client.unstableGetFileTreeSpace(roomId);
expect(tree).toBeDefined(); expect(tree).toBeDefined();
expect(tree.roomId).toEqual(roomId); expect(tree!.roomId).toEqual(roomId);
expect(tree.room).toBe(mockRoom); expect(tree!.room).toBe(mockRoom);
}); });
it("should not get (unstable) file trees if not joined", async () => { it("should not get (unstable) file trees if not joined", async () => {
const roomId = "!room:example.org"; const roomId = "!room:example.org";
const mockRoom = { const mockRoom = {
getMyMembership: () => "leave", // "not join" getMyMembership: () => "leave", // "not join"
}; } as unknown as Room;
client.getRoom = (getRoomId) => { client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId); expect(getRoomId).toEqual(roomId);
return mockRoom; return mockRoom;
@ -491,8 +528,8 @@ describe("MatrixClient", function() {
throw new Error("Unexpected event type or state key"); throw new Error("Unexpected event type or state key");
} }
}, },
}, } as Room["currentState"],
}; } as unknown as Room;
client.getRoom = (getRoomId) => { client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId); expect(getRoomId).toEqual(roomId);
return mockRoom; return mockRoom;
@ -525,8 +562,8 @@ describe("MatrixClient", function() {
throw new Error("Unexpected event type or state key"); throw new Error("Unexpected event type or state key");
} }
}, },
}, } as Room["currentState"],
}; } as unknown as Room;
client.getRoom = (getRoomId) => { client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId); expect(getRoomId).toEqual(roomId);
return mockRoom; return mockRoom;
@ -541,15 +578,15 @@ describe("MatrixClient", function() {
SYNC_RESPONSE, SYNC_RESPONSE,
]; ];
const filterId = "ehfewf"; const filterId = "ehfewf";
store.getFilterIdByName.mockReturnValue(filterId); mocked(store.getFilterIdByName).mockReturnValue(filterId);
const filter = new Filter("0", filterId); const filter = new Filter("0", filterId);
filter.setDefinition({ "room": { "timeline": { "limit": 8 } } }); filter.setDefinition({ "room": { "timeline": { "limit": 8 } } });
store.getFilter.mockReturnValue(filter); mocked(store.getFilter).mockReturnValue(filter);
const syncPromise = new Promise<void>((resolve, reject) => { const syncPromise = new Promise<void>((resolve, reject) => {
client.on("sync", function syncListener(state) { client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "SYNCING") { if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0); expect(httpLookups.length).toEqual(0);
client.removeListener("sync", syncListener); client.removeListener(ClientEvent.Sync, syncListener);
resolve(); resolve();
} else if (state === "ERROR") { } else if (state === "ERROR") {
reject(new Error("sync error")); reject(new Error("sync error"));
@ -567,10 +604,10 @@ describe("MatrixClient", function() {
it("should return the same sync state as emitted sync events", async function() { it("should return the same sync state as emitted sync events", async function() {
const syncingPromise = new Promise<void>((resolve) => { const syncingPromise = new Promise<void>((resolve) => {
client.on("sync", function syncListener(state) { client.on(ClientEvent.Sync, function syncListener(state) {
expect(state).toEqual(client.getSyncState()); expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") { if (state === "SYNCING") {
client.removeListener("sync", syncListener); client.removeListener(ClientEvent.Sync, syncListener);
resolve(); resolve();
} }
}); });
@ -586,7 +623,7 @@ describe("MatrixClient", function() {
it("should use an existing filter if id is present in localStorage", function() { it("should use an existing filter if id is present in localStorage", function() {
}); });
it("should handle localStorage filterId missing from the server", function(done) { it("should handle localStorage filterId missing from the server", function(done) {
function getFilterName(userId, suffix?: string) { function getFilterName(userId: string, suffix?: string) {
// scope this on the user ID because people may login on many accounts // scope this on the user ID because people may login on many accounts
// and they all need to be stored! // and they all need to be stored!
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : ""); return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
@ -605,14 +642,14 @@ describe("MatrixClient", function() {
}, },
}); });
httpLookups.push(FILTER_RESPONSE); httpLookups.push(FILTER_RESPONSE);
store.getFilterIdByName.mockReturnValue(invalidFilterId); mocked(store.getFilterIdByName).mockReturnValue(invalidFilterId);
const filterName = getFilterName(client.credentials.userId); const filterName = getFilterName(client.credentials.userId!);
client.store.setFilterIdByName(filterName, invalidFilterId); client.store.setFilterIdByName(filterName, invalidFilterId);
const filter = new Filter(client.credentials.userId); const filter = new Filter(client.credentials.userId);
client.getOrCreateFilter(filterName, filter).then(function(filterId) { client.getOrCreateFilter(filterName, filter).then(function(filterId) {
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id); expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
done(); done();
}); });
}); });
@ -634,13 +671,13 @@ describe("MatrixClient", function() {
httpLookups.push(FILTER_RESPONSE); httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE); httpLookups.push(SYNC_RESPONSE);
client.on("sync", function syncListener(state) { client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) { if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(2); expect(httpLookups.length).toEqual(2);
expect(client.retryImmediately()).toBe(true); expect(client.retryImmediately()).toBe(true);
jest.advanceTimersByTime(1); jest.advanceTimersByTime(1);
} else if (state === "PREPARED" && httpLookups.length === 0) { } else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener); client.removeListener(ClientEvent.Sync, syncListener);
done(); done();
} else { } else {
// unexpected state transition! // unexpected state transition!
@ -658,7 +695,7 @@ describe("MatrixClient", function() {
method: "GET", path: "/sync", data: SYNC_DATA, method: "GET", path: "/sync", data: SYNC_DATA,
}); });
client.on("sync", function syncListener(state) { client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) { if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1); expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe( expect(client.retryImmediately()).toBe(
@ -668,7 +705,7 @@ describe("MatrixClient", function() {
} else if (state === "RECONNECTING" && httpLookups.length > 0) { } else if (state === "RECONNECTING" && httpLookups.length > 0) {
jest.advanceTimersByTime(10000); jest.advanceTimersByTime(10000);
} else if (state === "SYNCING" && httpLookups.length === 0) { } else if (state === "SYNCING" && httpLookups.length === 0) {
client.removeListener("sync", syncListener); client.removeListener(ClientEvent.Sync, syncListener);
done(); done();
} }
}); });
@ -684,13 +721,13 @@ describe("MatrixClient", function() {
httpLookups.push(FILTER_RESPONSE); httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE); httpLookups.push(SYNC_RESPONSE);
client.on("sync", function syncListener(state) { client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) { if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(3); expect(httpLookups.length).toEqual(3);
expect(client.retryImmediately()).toBe(true); expect(client.retryImmediately()).toBe(true);
jest.advanceTimersByTime(1); jest.advanceTimersByTime(1);
} else if (state === "PREPARED" && httpLookups.length === 0) { } else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener); client.removeListener(ClientEvent.Sync, syncListener);
done(); done();
} else { } else {
// unexpected state transition! // unexpected state transition!
@ -702,8 +739,8 @@ describe("MatrixClient", function() {
}); });
describe("emitted sync events", function() { describe("emitted sync events", function() {
function syncChecker(expectedStates, done) { function syncChecker(expectedStates: [string, string | null][], done: Function) {
return function syncListener(state, old) { return function syncListener(state: SyncState, old: SyncState | null) {
const expected = expectedStates.shift(); const expected = expectedStates.shift();
logger.log( logger.log(
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected, "'sync' curr=%s old=%s EXPECT=%s", state, old, expected,
@ -715,7 +752,7 @@ describe("MatrixClient", function() {
expect(state).toEqual(expected[0]); expect(state).toEqual(expected[0]);
expect(old).toEqual(expected[1]); expect(old).toEqual(expected[1]);
if (expectedStates.length === 0) { if (expectedStates.length === 0) {
client.removeListener("sync", syncListener); client.removeListener(ClientEvent.Sync, syncListener);
done(); done();
} }
// standard retry time is 5 to 10 seconds // standard retry time is 5 to 10 seconds
@ -726,7 +763,7 @@ describe("MatrixClient", function() {
it("should transition null -> PREPARED after the first /sync", function(done) { it("should transition null -> PREPARED after the first /sync", function(done) {
const expectedStates: [string, string | null][] = []; const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]); expectedStates.push(["PREPARED", null]);
client.on("sync", syncChecker(expectedStates, done)); client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient(); client.startClient();
}); });
@ -738,7 +775,7 @@ describe("MatrixClient", function() {
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }, method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
}); });
expectedStates.push(["ERROR", null]); expectedStates.push(["ERROR", null]);
client.on("sync", syncChecker(expectedStates, done)); client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient(); client.startClient();
}); });
@ -768,7 +805,7 @@ describe("MatrixClient", function() {
expectedStates.push(["RECONNECTING", null]); expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]); expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]); expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done)); client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient(); client.startClient();
}); });
@ -776,7 +813,7 @@ describe("MatrixClient", function() {
const expectedStates: [string, string | null][] = []; const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]); expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]); expectedStates.push(["SYNCING", "PREPARED"]);
client.on("sync", syncChecker(expectedStates, done)); client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient(); client.startClient();
}); });
@ -795,7 +832,7 @@ describe("MatrixClient", function() {
expectedStates.push(["SYNCING", "PREPARED"]); expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["RECONNECTING", "SYNCING"]); expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]); expectedStates.push(["ERROR", "RECONNECTING"]);
client.on("sync", syncChecker(expectedStates, done)); client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient(); client.startClient();
}); });
@ -809,7 +846,7 @@ describe("MatrixClient", function() {
expectedStates.push(["PREPARED", null]); expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]); expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]); expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done)); client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient(); client.startClient();
}); });
@ -821,7 +858,7 @@ describe("MatrixClient", function() {
expectedStates.push(["PREPARED", null]); expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]); expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]); expectedStates.push(["SYNCING", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done)); client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient(); client.startClient();
}); });
@ -845,7 +882,7 @@ describe("MatrixClient", function() {
expectedStates.push(["RECONNECTING", "SYNCING"]); expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]); expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["ERROR", "ERROR"]); expectedStates.push(["ERROR", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done)); client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient(); client.startClient();
}); });
}); });
@ -914,14 +951,14 @@ describe("MatrixClient", function() {
throw new Error("Unexpected event type or state key"); throw new Error("Unexpected event type or state key");
} }
}, },
}, } as Room["currentState"],
getThread: jest.fn(), getThread: jest.fn(),
addPendingEvent: jest.fn(), addPendingEvent: jest.fn(),
updatePendingEvent: jest.fn(), updatePendingEvent: jest.fn(),
reEmitter: { reEmitter: {
reEmit: jest.fn(), reEmit: jest.fn(),
}, },
}; } as unknown as Room;
beforeEach(() => { beforeEach(() => {
client.getRoom = (getRoomId) => { client.getRoom = (getRoomId) => {
@ -987,7 +1024,7 @@ describe("MatrixClient", function() {
const mockRoom = { const mockRoom = {
getMyMembership: () => "join", getMyMembership: () => "join",
updatePendingEvent: (event, status) => event.setStatus(status), updatePendingEvent: (event: MatrixEvent, status: EventStatus) => event.setStatus(status),
currentState: { currentState: {
getStateEvents: (eventType, stateKey) => { getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) { if (eventType === EventType.RoomCreate) {
@ -1004,15 +1041,14 @@ describe("MatrixClient", function() {
throw new Error("Unexpected event type or state key"); throw new Error("Unexpected event type or state key");
} }
}, },
}, } as Room["currentState"],
}; } as unknown as Room;
let event; let event: MatrixEvent;
beforeEach(async () => { beforeEach(async () => {
event = new MatrixEvent({ event = new MatrixEvent({
event_id: "~" + roomId + ":" + txnId, event_id: "~" + roomId + ":" + txnId,
user_id: client.credentials.userId, sender: client.credentials.userId!,
sender: client.credentials.userId,
room_id: roomId, room_id: roomId,
origin_server_ts: new Date().getTime(), origin_server_ts: new Date().getTime(),
}); });
@ -1023,25 +1059,26 @@ describe("MatrixClient", function() {
return mockRoom; return mockRoom;
}; };
client.crypto = { // mock crypto client.crypto = { // mock crypto
encryptEvent: (event, room) => new Promise(() => {}), encryptEvent: () => new Promise(() => {}),
stop: jest.fn(), stop: jest.fn(),
}; } as unknown as Crypto;
}); });
function assertCancelled() { function assertCancelled() {
expect(event.status).toBe(EventStatus.CANCELLED); expect(event.status).toBe(EventStatus.CANCELLED);
expect(client.scheduler.removeEventFromQueue(event)).toBeFalsy(); expect(client.scheduler?.removeEventFromQueue(event)).toBeFalsy();
expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0); expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0);
} }
it("should cancel an event which is queued", () => { it("should cancel an event which is queued", () => {
event.setStatus(EventStatus.QUEUED); event.setStatus(EventStatus.QUEUED);
client.scheduler.queueEvent(event); client.scheduler?.queueEvent(event);
client.cancelPendingEvent(event); client.cancelPendingEvent(event);
assertCancelled(); assertCancelled();
}); });
it("should cancel an event which is encrypting", async () => { it("should cancel an event which is encrypting", async () => {
// @ts-ignore protected method access
client.encryptAndSendEvent(null, event); client.encryptAndSendEvent(null, event);
await testUtils.emitPromise(event, "Event.status"); await testUtils.emitPromise(event, "Event.status");
client.cancelPendingEvent(event); client.cancelPendingEvent(event);
@ -1103,7 +1140,7 @@ describe("MatrixClient", function() {
const room = { const room = {
hasPendingEvent: jest.fn().mockReturnValue(false), hasPendingEvent: jest.fn().mockReturnValue(false),
addLocalEchoReceipt: jest.fn(), addLocalEchoReceipt: jest.fn(),
}; } as unknown as Room;
const rrEvent = new MatrixEvent({ event_id: "read_event_id" }); const rrEvent = new MatrixEvent({ event_id: "read_event_id" });
const rpEvent = new MatrixEvent({ event_id: "read_private_event_id" }); const rpEvent = new MatrixEvent({ event_id: "read_private_event_id" });
client.getRoom = () => room; client.getRoom = () => room;
@ -1142,7 +1179,7 @@ describe("MatrixClient", function() {
const content = makeBeaconInfoContent(100, true); const content = makeBeaconInfoContent(100, true);
beforeEach(() => { beforeEach(() => {
client.http.authedRequest.mockClear().mockResolvedValue({}); mocked(client.http.authedRequest).mockClear().mockResolvedValue({});
}); });
it("creates new beacon info", async () => { it("creates new beacon info", async () => {
@ -1150,7 +1187,7 @@ describe("MatrixClient", function() {
// event type combined // event type combined
const expectedEventType = M_BEACON_INFO.name; const expectedEventType = M_BEACON_INFO.name;
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0]; const [method, path, queryParams, requestContent] = mocked(client.http.authedRequest).mock.calls[0];
expect(method).toBe('PUT'); expect(method).toBe('PUT');
expect(path).toEqual( expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` + `/rooms/${encodeURIComponent(roomId)}/state/` +
@ -1164,7 +1201,7 @@ describe("MatrixClient", function() {
await client.unstable_setLiveBeacon(roomId, content); await client.unstable_setLiveBeacon(roomId, content);
// event type combined // event type combined
const [, path, , requestContent] = client.http.authedRequest.mock.calls[0]; const [, path, , requestContent] = mocked(client.http.authedRequest).mock.calls[0];
expect(path).toEqual( expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` + `/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`, `${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`,
@ -1242,7 +1279,7 @@ describe("MatrixClient", function() {
const newPassword = 'newpassword'; const newPassword = 'newpassword';
const passwordTest = (expectedRequestContent: any) => { const passwordTest = (expectedRequestContent: any) => {
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0]; const [method, path, queryParams, requestContent] = mocked(client.http.authedRequest).mock.calls[0];
expect(method).toBe('POST'); expect(method).toBe('POST');
expect(path).toEqual('/account/password'); expect(path).toEqual('/account/password');
expect(queryParams).toBeFalsy(); expect(queryParams).toBeFalsy();
@ -1250,7 +1287,7 @@ describe("MatrixClient", function() {
}; };
beforeEach(() => { beforeEach(() => {
client.http.authedRequest.mockClear().mockResolvedValue({}); mocked(client.http.authedRequest).mockClear().mockResolvedValue({});
}); });
it("no logout_devices specified", async () => { it("no logout_devices specified", async () => {
@ -1289,13 +1326,13 @@ describe("MatrixClient", function() {
const response = { const response = {
aliases: ["#woop:example.org", "#another:example.org"], aliases: ["#woop:example.org", "#another:example.org"],
}; };
client.http.authedRequest.mockClear().mockResolvedValue(response); mocked(client.http.authedRequest).mockClear().mockResolvedValue(response);
const roomId = "!whatever:example.org"; const roomId = "!whatever:example.org";
const result = await client.getLocalAliases(roomId); const result = await client.getLocalAliases(roomId);
// Current version of the endpoint we support is v3 // Current version of the endpoint we support is v3
const [method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0]; const [method, path, queryParams, data, opts] = mocked(client.http.authedRequest).mock.calls[0];
expect(data).toBeFalsy(); expect(data).toBeFalsy();
expect(method).toBe('GET'); expect(method).toBe('GET');
expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`); expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`);
@ -1352,11 +1389,11 @@ describe("MatrixClient", function() {
], ],
username: "1443779631:@user:example.com", username: "1443779631:@user:example.com",
password: "JlKfBy1QwLrO20385QyAtEyIv0=", password: "JlKfBy1QwLrO20385QyAtEyIv0=",
}; } as unknown as ITurnServerResponse;
jest.spyOn(client, "turnServer").mockResolvedValue(turnServer); jest.spyOn(client, "turnServer").mockResolvedValue(turnServer);
const events: any[][] = []; const events: any[][] = [];
const onTurnServers = (...args) => events.push(args); const onTurnServers = (...args: any[]) => events.push(args);
client.on(ClientEvent.TurnServers, onTurnServers); client.on(ClientEvent.TurnServers, onTurnServers);
expect(await client.checkTurnServers()).toBe(true); expect(await client.checkTurnServers()).toBe(true);
client.off(ClientEvent.TurnServers, onTurnServers); client.off(ClientEvent.TurnServers, onTurnServers);
@ -1372,7 +1409,7 @@ describe("MatrixClient", function() {
jest.spyOn(client, "turnServer").mockRejectedValue(error); jest.spyOn(client, "turnServer").mockRejectedValue(error);
const events: any[][] = []; const events: any[][] = [];
const onTurnServersError = (...args) => events.push(args); const onTurnServersError = (...args: any[]) => events.push(args);
client.on(ClientEvent.TurnServersError, onTurnServersError); client.on(ClientEvent.TurnServersError, onTurnServersError);
expect(await client.checkTurnServers()).toBe(false); expect(await client.checkTurnServers()).toBe(false);
client.off(ClientEvent.TurnServersError, onTurnServersError); client.off(ClientEvent.TurnServersError, onTurnServersError);
@ -1384,7 +1421,7 @@ describe("MatrixClient", function() {
jest.spyOn(client, "turnServer").mockRejectedValue(error); jest.spyOn(client, "turnServer").mockRejectedValue(error);
const events: any[][] = []; const events: any[][] = [];
const onTurnServersError = (...args) => events.push(args); const onTurnServersError = (...args: any[]) => events.push(args);
client.on(ClientEvent.TurnServersError, onTurnServersError); client.on(ClientEvent.TurnServersError, onTurnServersError);
expect(await client.checkTurnServers()).toBe(false); expect(await client.checkTurnServers()).toBe(false);
client.off(ClientEvent.TurnServersError, onTurnServersError); client.off(ClientEvent.TurnServersError, onTurnServersError);
@ -1400,7 +1437,7 @@ describe("MatrixClient", function() {
it("is an alias for the crypto method", async () => { it("is an alias for the crypto method", async () => {
client.crypto = testUtils.mock(Crypto, "Crypto"); client.crypto = testUtils.mock(Crypto, "Crypto");
const deviceInfos = []; const deviceInfos: IOlmDevice[] = [];
const payload = {}; const payload = {};
await client.encryptAndSendToDevices(deviceInfos, payload); await client.encryptAndSendToDevices(deviceInfos, payload);
expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload); expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload);
@ -1413,7 +1450,7 @@ describe("MatrixClient", function() {
const dataStore = new Map(); const dataStore = new Map();
client.setAccountData = function(eventType, content) { client.setAccountData = function(eventType, content) {
dataStore.set(eventType, content); dataStore.set(eventType, content);
return Promise.resolve(); return Promise.resolve({});
}; };
client.getAccountData = function(eventType) { client.getAccountData = function(eventType) {
const data = dataStore.get(eventType); const data = dataStore.get(eventType);
@ -1424,9 +1461,9 @@ describe("MatrixClient", function() {
// Mockup `createRoom`/`getRoom`/`joinRoom`, including state. // Mockup `createRoom`/`getRoom`/`joinRoom`, including state.
const rooms = new Map(); const rooms = new Map();
client.createRoom = function(options = {}) { client.createRoom = function(options: Options = {}) {
const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`; const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`;
const state = new Map(); const state = new Map<string, any>();
const room = { const room = {
roomId, roomId,
_options: options, _options: options,
@ -1444,24 +1481,24 @@ describe("MatrixClient", function() {
}, },
}; };
}, },
}; } as EventTimeline;
}, },
}; };
}, },
}; } as unknown as WrappedRoom;
rooms.set(roomId, room); rooms.set(roomId, room);
return Promise.resolve({ room_id: roomId }); return Promise.resolve({ room_id: roomId });
}; };
client.getRoom = function(roomId) { client.getRoom = function(roomId) {
return rooms.get(roomId); return rooms.get(roomId);
}; };
client.joinRoom = function(roomId) { client.joinRoom = async function(roomId) {
return this.getRoom(roomId) || this.createRoom({ _roomId: roomId }); return this.getRoom(roomId)! || this.createRoom({ _roomId: roomId } as ICreateRoomOpts);
}; };
// Mockup state events // Mockup state events
client.sendStateEvent = function(roomId, type, content) { client.sendStateEvent = function(roomId, type, content) {
const room = this.getRoom(roomId); const room = this.getRoom(roomId) as WrappedRoom;
const state: Map<string, any> = room._state; const state: Map<string, any> = room._state;
let store = state.get(type); let store = state.get(type);
if (!store) { if (!store) {
@ -1480,14 +1517,15 @@ describe("MatrixClient", function() {
return content; return content;
}, },
}; };
return { event_id: eventId }; return Promise.resolve({ event_id: eventId });
}; };
client.redactEvent = function(roomId, eventId) { client.redactEvent = function(roomId, eventId) {
const room = this.getRoom(roomId); const room = this.getRoom(roomId) as WrappedRoom;
const state: Map<string, any> = room._state; const state: Map<string, any> = room._state;
for (const store of state.values()) { for (const store of state.values()) {
delete store[eventId]; delete store[eventId!];
} }
return Promise.resolve({ event_id: "$" + eventId + "-" + Math.random() });
}; };
}); });
@ -1523,7 +1561,7 @@ describe("MatrixClient", function() {
roomId: "!snafu:somewhere.org", roomId: "!snafu:somewhere.org",
}); });
expect(ruleMatch).toBeTruthy(); expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({ expect(ruleMatch!.getContent()).toMatchObject({
recommendation: "m.ban", recommendation: "m.ban",
reason: "just a test", reason: "just a test",
}); });
@ -1552,7 +1590,7 @@ describe("MatrixClient", function() {
roomId: "!snafu:somewhere.org", roomId: "!snafu:somewhere.org",
}); });
expect(ruleSenderMatch).toBeTruthy(); expect(ruleSenderMatch).toBeTruthy();
expect(ruleSenderMatch.getContent()).toMatchObject({ expect(ruleSenderMatch!.getContent()).toMatchObject({
recommendation: "m.ban", recommendation: "m.ban",
reason: REASON, reason: REASON,
}); });
@ -1562,7 +1600,7 @@ describe("MatrixClient", function() {
roomId: "!snafu:example.org", roomId: "!snafu:example.org",
}); });
expect(ruleRoomMatch).toBeTruthy(); expect(ruleRoomMatch).toBeTruthy();
expect(ruleRoomMatch.getContent()).toMatchObject({ expect(ruleRoomMatch!.getContent()).toMatchObject({
recommendation: "m.ban", recommendation: "m.ban",
reason: REASON, reason: REASON,
}); });
@ -1587,7 +1625,7 @@ describe("MatrixClient", function() {
roomId: BAD_ROOM_ID, roomId: BAD_ROOM_ID,
}); });
expect(ruleSenderMatch).toBeTruthy(); expect(ruleSenderMatch).toBeTruthy();
expect(ruleSenderMatch.getContent()).toMatchObject({ expect(ruleSenderMatch!.getContent()).toMatchObject({
recommendation: "m.ban", recommendation: "m.ban",
reason: REASON, reason: REASON,
}); });
@ -1621,7 +1659,7 @@ describe("MatrixClient", function() {
roomId: "!snafu:somewhere.org", roomId: "!snafu:somewhere.org",
}); });
expect(ruleMatch).toBeTruthy(); expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({ expect(ruleMatch!.getContent()).toMatchObject({
recommendation: "m.ban", recommendation: "m.ban",
reason: "just a test", reason: "just a test",
}); });
@ -1649,13 +1687,13 @@ describe("MatrixClient", function() {
roomId: "!snafu:somewhere.org", roomId: "!snafu:somewhere.org",
}); });
expect(ruleMatch).toBeTruthy(); expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({ expect(ruleMatch!.getContent()).toMatchObject({
recommendation: "m.ban", recommendation: "m.ban",
reason: "just a test", reason: "just a test",
}); });
// After removing the invite, we shouldn't reject it anymore. // After removing the invite, we shouldn't reject it anymore.
await client.ignoredInvites.removeRule(ruleMatch); await client.ignoredInvites.removeRule(ruleMatch as MatrixEvent);
const ruleMatch2 = await client.ignoredInvites.getRuleForInvite({ const ruleMatch2 = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org", sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org", roomId: "!snafu:somewhere.org",
@ -1669,10 +1707,10 @@ describe("MatrixClient", function() {
// Make sure that everything is initialized. // Make sure that everything is initialized.
await client.ignoredInvites.getOrCreateSourceRooms(); await client.ignoredInvites.getOrCreateSourceRooms();
await client.joinRoom(NEW_SOURCE_ROOM_ID); await client.joinRoom(NEW_SOURCE_ROOM_ID);
const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID); const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID) as WrappedRoom;
// Fetch the list of sources and check that we do not have the new room yet. // Fetch the list of sources and check that we do not have the new room yet.
const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent(); const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name)!.getContent();
expect(policies).toBeTruthy(); expect(policies).toBeTruthy();
const ignoreInvites = policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name]; const ignoreInvites = policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
expect(ignoreInvites).toBeTruthy(); expect(ignoreInvites).toBeTruthy();
@ -1686,7 +1724,7 @@ describe("MatrixClient", function() {
expect(added2).toBe(false); expect(added2).toBe(false);
// Fetch the list of sources and check that we have added the new room. // Fetch the list of sources and check that we have added the new room.
const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent(); const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name)!.getContent();
expect(policies2).toBeTruthy(); expect(policies2).toBeTruthy();
const ignoreInvites2 = policies2[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name]; const ignoreInvites2 = policies2[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
expect(ignoreInvites2).toBeTruthy(); expect(ignoreInvites2).toBeTruthy();
@ -1698,7 +1736,7 @@ describe("MatrixClient", function() {
// Check where it shows up. // Check where it shows up.
const targetRoomId = ignoreInvites2.target; const targetRoomId = ignoreInvites2.target;
const targetRoom = client.getRoom(targetRoomId); const targetRoom = client.getRoom(targetRoomId) as WrappedRoom;
expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy(); expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy();
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy(); expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
}); });

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClient } from "../../../src"; import { IContent, MatrixClient } from "../../../src";
import { Room } from "../../../src/models/room"; import { Room } from "../../../src/models/room";
import { MatrixEvent } from "../../../src/models/event"; import { MatrixEvent } from "../../../src/models/event";
import { EventType, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../../../src/@types/event"; import { EventType, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../../../src/@types/event";
@ -33,7 +33,7 @@ describe("MSC3089TreeSpace", () => {
const roomId = "!tree:localhost"; const roomId = "!tree:localhost";
const targetUser = "@target:example.org"; const targetUser = "@target:example.org";
let powerLevels; let powerLevels: MatrixEvent;
beforeEach(() => { beforeEach(() => {
// TODO: Use utility functions to create test rooms and clients // TODO: Use utility functions to create test rooms and clients
@ -480,7 +480,7 @@ describe("MSC3089TreeSpace", () => {
const staticDomain = "static.example.org"; const staticDomain = "static.example.org";
function addSubspace(roomId: string, createTs?: number, order?: string) { function addSubspace(roomId: string, createTs?: number, order?: string) {
const content = { const content: IContent = {
via: [staticDomain], via: [staticDomain],
}; };
if (order) content['order'] = order; if (order) content['order'] = order;

View File

@ -121,7 +121,7 @@ describe('MatrixEvent', () => {
}); });
describe(".attemptDecryption", () => { describe(".attemptDecryption", () => {
let encryptedEvent; let encryptedEvent: MatrixEvent;
const eventId = 'test_encrypted_event'; const eventId = 'test_encrypted_event';
beforeEach(() => { beforeEach(() => {
@ -155,7 +155,7 @@ describe('MatrixEvent', () => {
}, },
}); });
}), }),
}; } as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto); await encryptedEvent.attemptDecryption(crypto);

View File

@ -39,7 +39,7 @@ let threadEvent: MatrixEvent;
const ROOM_ID = "!roomId:example.org"; const ROOM_ID = "!roomId:example.org";
let THREAD_ID: string; let THREAD_ID: string;
function mkPushAction(notify, highlight): IActionsObject { function mkPushAction(notify: boolean, highlight: boolean): IActionsObject {
return { return {
notify, notify,
tweaks: { tweaks: {

View File

@ -70,7 +70,7 @@ const roomEvent = utils.mkEvent({
}, },
}); });
function mockServerSideSupport(client, serverSideSupport: ServerSupport) { function mockServerSideSupport(client: MatrixClient, serverSideSupport: ServerSupport) {
client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport); client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport);
} }

View File

@ -20,7 +20,7 @@ let wallTime = 1234567890;
jest.useFakeTimers().setSystemTime(wallTime); jest.useFakeTimers().setSystemTime(wallTime);
describe("realtime-callbacks", function() { describe("realtime-callbacks", function() {
function tick(millis) { function tick(millis: number): void {
wallTime += millis; wallTime += millis;
jest.advanceTimersByTime(millis); jest.advanceTimersByTime(millis);
} }

View File

@ -87,7 +87,7 @@ describe("Rendezvous", function() {
}); });
let httpBackend: MockHttpBackend; let httpBackend: MockHttpBackend;
let fetchFn: typeof global.fetchFn; let fetchFn: typeof global.fetch;
let transports: DummyTransport<any, MSC3903ECDHPayload>[]; let transports: DummyTransport<any, MSC3903ECDHPayload>[];
beforeEach(function() { beforeEach(function() {

View File

@ -373,37 +373,36 @@ describe("RoomMember", function() {
expect(member.events.member).toEqual(joinEvent); expect(member.events.member).toEqual(joinEvent);
}); });
it("should set 'name' based on user_id, displayname and room state", it("should set 'name' based on user_id, displayname and room state", function() {
function() { const roomState = {
const roomState = { getStateEvents: function(type: string) {
getStateEvents: function(type) { if (type !== "m.room.member") {
if (type !== "m.room.member") { return [];
return []; }
} return [
return [ utils.mkMembership({
utils.mkMembership({ event: true, mship: "join", room: roomId,
event: true, mship: "join", room: roomId, user: userB,
user: userB, }),
}), utils.mkMembership({
utils.mkMembership({ event: true, mship: "join", room: roomId,
event: true, mship: "join", room: roomId, user: userC, name: "Alice",
user: userC, name: "Alice", }),
}), joinEvent,
joinEvent, ];
]; },
}, getUserIdsWithDisplayName: function(displayName: string) {
getUserIdsWithDisplayName: function(displayName) { return [userA, userC];
return [userA, userC]; },
}, } as unknown as RoomState;
} as unknown as RoomState; expect(member.name).toEqual(userA); // default = user_id
expect(member.name).toEqual(userA); // default = user_id member.setMembershipEvent(joinEvent);
member.setMembershipEvent(joinEvent); expect(member.name).toEqual("Alice"); // prefer displayname
expect(member.name).toEqual("Alice"); // prefer displayname member.setMembershipEvent(joinEvent, roomState);
member.setMembershipEvent(joinEvent, roomState); expect(member.name).not.toEqual("Alice"); // it should disambig.
expect(member.name).not.toEqual("Alice"); // it should disambig. // user_id should be there somewhere
// user_id should be there somewhere expect(member.name.indexOf(userA)).not.toEqual(-1);
expect(member.name.indexOf(userA)).not.toEqual(-1); });
});
it("should emit 'RoomMember.membership' if the membership changes", function() { it("should emit 'RoomMember.membership' if the membership changes", function() {
let emitCount = 0; let emitCount = 0;
@ -455,7 +454,7 @@ describe("RoomMember", function() {
}); });
const roomState = { const roomState = {
getStateEvents: function(type) { getStateEvents: function(type: string) {
if (type !== "m.room.member") { if (type !== "m.room.member") {
return []; return [];
} }
@ -467,7 +466,7 @@ describe("RoomMember", function() {
joinEvent, joinEvent,
]; ];
}, },
getUserIdsWithDisplayName: function(displayName) { getUserIdsWithDisplayName: function(displayName: string) {
return [userA, userC]; return [userA, userC];
}, },
} as unknown as RoomState; } as unknown as RoomState;

View File

@ -19,27 +19,32 @@ limitations under the License.
* @module client * @module client
*/ */
import { mocked } from "jest-mock";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { emitPromise } from "../test-utils/test-utils";
import { import {
Direction,
DuplicateStrategy, DuplicateStrategy,
EventStatus, EventStatus,
EventTimelineSet, EventTimelineSet,
EventType, IStateEventWithRoomId, EventType, IContent,
IStateEventWithRoomId,
JoinRule, JoinRule,
MatrixEvent, MatrixEvent,
MatrixEventEvent, MatrixEventEvent,
PendingEventOrdering, PendingEventOrdering,
RelationType, RelationType,
RoomEvent, RoomEvent,
RoomMember,
} from "../../src"; } from "../../src";
import { EventTimeline } from "../../src/models/event-timeline"; import { EventTimeline } from "../../src/models/event-timeline";
import { NotificationCountType, Room } from "../../src/models/room"; import { NotificationCountType, Room } from "../../src/models/room";
import { RoomState } from "../../src/models/room-state"; import { RoomState } from "../../src/models/room-state";
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
import { emitPromise } from "../test-utils/test-utils";
import { ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts"; import { ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts";
import { FeatureSupport, Thread, ThreadEvent, THREAD_RELATION_TYPE } from "../../src/models/thread"; import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread";
import { Crypto } from "../../src/crypto"; import { Crypto } from "../../src/crypto";
describe("Room", function() { describe("Room", function() {
@ -48,7 +53,7 @@ describe("Room", function() {
const userB = "@bertha:bar"; const userB = "@bertha:bar";
const userC = "@clarissa:bar"; const userC = "@clarissa:bar";
const userD = "@dorothy:bar"; const userD = "@dorothy:bar";
let room; let room: Room;
const mkMessage = () => utils.mkMessage({ const mkMessage = () => utils.mkMessage({
event: true, event: true,
@ -131,13 +136,16 @@ describe("Room", function() {
beforeEach(function() { beforeEach(function() {
room = new Room(roomId, new TestClient(userA, "device").client, userA); room = new Room(roomId, new TestClient(userA, "device").client, userA);
// mock RoomStates // mock RoomStates
// @ts-ignore
room.oldState = room.getLiveTimeline().startState = utils.mock(RoomState, "oldState"); room.oldState = room.getLiveTimeline().startState = utils.mock(RoomState, "oldState");
// @ts-ignore
room.currentState = room.getLiveTimeline().endState = utils.mock(RoomState, "currentState"); room.currentState = room.getLiveTimeline().endState = utils.mock(RoomState, "currentState");
}); });
describe('getCreator', () => { describe('getCreator', () => {
it("should return the creator from m.room.create", function() { it("should return the creator from m.room.create", function() {
room.currentState.getStateEvents.mockImplementation(function(type, key) { // @ts-ignore - mocked doesn't handle overloads sanely
mocked(room.currentState.getStateEvents).mockImplementation(function(type, key) {
if (type === EventType.RoomCreate && key === "") { if (type === EventType.RoomCreate && key === "") {
return utils.mkEvent({ return utils.mkEvent({
event: true, event: true,
@ -160,7 +168,8 @@ describe("Room", function() {
const hsUrl = "https://my.home.server"; const hsUrl = "https://my.home.server";
it("should return the URL from m.room.avatar preferentially", function() { it("should return the URL from m.room.avatar preferentially", function() {
room.currentState.getStateEvents.mockImplementation(function(type, key) { // @ts-ignore - mocked doesn't handle overloads sanely
mocked(room.currentState.getStateEvents).mockImplementation(function(type, key) {
if (type === EventType.RoomAvatar && key === "") { if (type === EventType.RoomAvatar && key === "") {
return utils.mkEvent({ return utils.mkEvent({
event: true, event: true,
@ -174,10 +183,10 @@ describe("Room", function() {
}); });
} }
}); });
const url = room.getAvatarUrl(hsUrl); const url = room.getAvatarUrl(hsUrl, 100, 100, "scale");
// we don't care about how the mxc->http conversion is done, other // we don't care about how the mxc->http conversion is done, other
// than it contains the mxc body. // than it contains the mxc body.
expect(url.indexOf("flibble/wibble")).not.toEqual(-1); expect(url?.indexOf("flibble/wibble")).not.toEqual(-1);
}); });
it("should return nothing if there is no m.room.avatar and allowDefault=false", it("should return nothing if there is no m.room.avatar and allowDefault=false",
@ -189,12 +198,12 @@ describe("Room", function() {
describe("getMember", function() { describe("getMember", function() {
beforeEach(function() { beforeEach(function() {
room.currentState.getMember.mockImplementation(function(userId) { mocked(room.currentState.getMember).mockImplementation(function(userId) {
return { return {
"@alice:bar": { "@alice:bar": {
userId: userA, userId: userA,
roomId: roomId, roomId: roomId,
}, } as unknown as RoomMember,
}[userId] || null; }[userId] || null;
}); });
}); });
@ -222,11 +231,13 @@ describe("Room", function() {
it("Make sure legacy overload passing options directly as parameters still works", () => { it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => room.addLiveEvents(events, DuplicateStrategy.Replace, false)).not.toThrow(); expect(() => room.addLiveEvents(events, DuplicateStrategy.Replace, false)).not.toThrow();
expect(() => room.addLiveEvents(events, DuplicateStrategy.Ignore, true)).not.toThrow(); expect(() => room.addLiveEvents(events, DuplicateStrategy.Ignore, true)).not.toThrow();
// @ts-ignore
expect(() => room.addLiveEvents(events, "shouldfailbecauseinvalidduplicatestrategy", false)).toThrow(); expect(() => room.addLiveEvents(events, "shouldfailbecauseinvalidduplicatestrategy", false)).toThrow();
}); });
it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function() { it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function() {
expect(function() { expect(function() {
// @ts-ignore
room.addLiveEvents(events, { room.addLiveEvents(events, {
duplicateStrategy: "foo", duplicateStrategy: "foo",
}); });
@ -255,6 +266,7 @@ describe("Room", function() {
dupe.event.event_id = events[0].getId(); dupe.event.event_id = events[0].getId();
room.addLiveEvents(events); room.addLiveEvents(events);
expect(room.timeline[0]).toEqual(events[0]); expect(room.timeline[0]).toEqual(events[0]);
// @ts-ignore
room.addLiveEvents([dupe], { room.addLiveEvents([dupe], {
duplicateStrategy: "ignore", duplicateStrategy: "ignore",
}); });
@ -263,7 +275,7 @@ describe("Room", function() {
it("should emit 'Room.timeline' events", function() { it("should emit 'Room.timeline' events", function() {
let callCount = 0; let callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) { room.on(RoomEvent.Timeline, function(event, emitRoom, toStart) {
callCount += 1; callCount += 1;
expect(room.timeline.length).toEqual(callCount); expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]); expect(event).toEqual(events[callCount - 1]);
@ -306,8 +318,8 @@ describe("Room", function() {
userId: userA, userId: userA,
membership: "join", membership: "join",
name: "Alice", name: "Alice",
}; } as unknown as RoomMember;
room.currentState.getSentinelMember.mockImplementation(function(uid) { mocked(room.currentState.getSentinelMember).mockImplementation(function(uid) {
if (uid === userA) { if (uid === userA) {
return sentinel; return sentinel;
} }
@ -331,27 +343,25 @@ describe("Room", function() {
const remoteEventId = remoteEvent.getId(); const remoteEventId = remoteEvent.getId();
let callCount = 0; let callCount = 0;
room.on("Room.localEchoUpdated", room.on(RoomEvent.LocalEchoUpdated, (event, emitRoom, oldEventId, oldStatus) => {
function(event, emitRoom, oldEventId, oldStatus) { switch (callCount) {
switch (callCount) { case 0:
case 0: expect(event.getId()).toEqual(localEventId);
expect(event.getId()).toEqual(localEventId); expect(event.status).toEqual(EventStatus.SENDING);
expect(event.status).toEqual(EventStatus.SENDING); expect(emitRoom).toEqual(room);
expect(emitRoom).toEqual(room); expect(oldEventId).toBeUndefined();
expect(oldEventId).toBeUndefined(); expect(oldStatus).toBeUndefined();
expect(oldStatus).toBeUndefined(); break;
break; case 1:
case 1: expect(event.getId()).toEqual(remoteEventId);
expect(event.getId()).toEqual(remoteEventId); expect(event.status).toBeNull();
expect(event.status).toBeNull(); expect(emitRoom).toEqual(room);
expect(emitRoom).toEqual(room); expect(oldEventId).toEqual(localEventId);
expect(oldEventId).toEqual(localEventId); expect(oldStatus).toBe(EventStatus.SENDING);
expect(oldStatus).toBe(EventStatus.SENDING); break;
break; }
} callCount += 1;
callCount += 1; });
},
);
// first add the local echo // first add the local echo
room.addPendingEvent(localEvent, "TXN_ID"); room.addPendingEvent(localEvent, "TXN_ID");
@ -367,7 +377,7 @@ describe("Room", function() {
it("should be able to update local echo without a txn ID (/send then /sync)", function() { it("should be able to update local echo without a txn ID (/send then /sync)", function() {
const eventJson = utils.mkMessage({ const eventJson = utils.mkMessage({
room: roomId, user: userA, event: false, room: roomId, user: userA, event: false,
}) as object; });
delete eventJson["txn_id"]; delete eventJson["txn_id"];
delete eventJson["event_id"]; delete eventJson["event_id"];
const localEvent = new MatrixEvent(Object.assign({ event_id: "$temp" }, eventJson)); const localEvent = new MatrixEvent(Object.assign({ event_id: "$temp" }, eventJson));
@ -398,7 +408,7 @@ describe("Room", function() {
it("should be able to update local echo without a txn ID (/sync then /send)", function() { it("should be able to update local echo without a txn ID (/sync then /send)", function() {
const eventJson = utils.mkMessage({ const eventJson = utils.mkMessage({
room: roomId, user: userA, event: false, room: roomId, user: userA, event: false,
}) as object; });
delete eventJson["txn_id"]; delete eventJson["txn_id"];
delete eventJson["event_id"]; delete eventJson["event_id"];
const txnId = "My_txn_id"; const txnId = "My_txn_id";
@ -483,7 +493,7 @@ describe("Room", function() {
it("should emit 'Room.timeline' events when added to the start", it("should emit 'Room.timeline' events when added to the start",
function() { function() {
let callCount = 0; let callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) { room.on(RoomEvent.Timeline, function(event, emitRoom, toStart) {
callCount += 1; callCount += 1;
expect(room.timeline.length).toEqual(callCount); expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]); expect(event).toEqual(events[callCount - 1]);
@ -501,19 +511,19 @@ describe("Room", function() {
userId: userA, userId: userA,
membership: "join", membership: "join",
name: "Alice", name: "Alice",
}; } as unknown as RoomMember;
const oldSentinel = { const oldSentinel = {
userId: userA, userId: userA,
membership: "join", membership: "join",
name: "Old Alice", name: "Old Alice",
}; } as unknown as RoomMember;
room.currentState.getSentinelMember.mockImplementation(function(uid) { mocked(room.currentState.getSentinelMember).mockImplementation(function(uid) {
if (uid === userA) { if (uid === userA) {
return sentinel; return sentinel;
} }
return null; return null;
}); });
room.oldState.getSentinelMember.mockImplementation(function(uid) { mocked(room.oldState.getSentinelMember).mockImplementation(function(uid) {
if (uid === userA) { if (uid === userA) {
return oldSentinel; return oldSentinel;
} }
@ -539,19 +549,19 @@ describe("Room", function() {
userId: userA, userId: userA,
membership: "join", membership: "join",
name: "Alice", name: "Alice",
}; } as unknown as RoomMember;
const oldSentinel = { const oldSentinel = {
userId: userA, userId: userA,
membership: "join", membership: "join",
name: "Old Alice", name: "Old Alice",
}; } as unknown as RoomMember;
room.currentState.getSentinelMember.mockImplementation(function(uid) { mocked(room.currentState.getSentinelMember).mockImplementation(function(uid) {
if (uid === userA) { if (uid === userA) {
return sentinel; return sentinel;
} }
return null; return null;
}); });
room.oldState.getSentinelMember.mockImplementation(function(uid) { mocked(room.oldState.getSentinelMember).mockImplementation(function(uid) {
if (uid === userA) { if (uid === userA) {
return oldSentinel; return oldSentinel;
} }
@ -599,7 +609,7 @@ describe("Room", function() {
}); });
}); });
const resetTimelineTests = function(timelineSupport) { const resetTimelineTests = function(timelineSupport: boolean) {
let events: MatrixEvent[]; let events: MatrixEvent[];
beforeEach(function() { beforeEach(function() {
@ -630,8 +640,8 @@ describe("Room", function() {
const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS); const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
const newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS); const newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
expect(room.getLiveTimeline().getEvents().length).toEqual(1); expect(room.getLiveTimeline().getEvents().length).toEqual(1);
expect(oldState.getStateEvents(EventType.RoomName, "")).toEqual(events[1]); expect(oldState?.getStateEvents(EventType.RoomName, "")).toEqual(events[1]);
expect(newState.getStateEvents(EventType.RoomName, "")).toEqual(events[2]); expect(newState?.getStateEvents(EventType.RoomName, "")).toEqual(events[2]);
}); });
it("should reset the legacy timeline fields", function() { it("should reset the legacy timeline fields", function() {
@ -669,17 +679,14 @@ describe("Room", function() {
expect(currentStateUpdateEmitCount).toEqual(timelineSupport ? 1 : 0); expect(currentStateUpdateEmitCount).toEqual(timelineSupport ? 1 : 0);
}); });
it("should emit Room.timelineReset event and set the correct " + it("should emit Room.timelineReset event and set the correct pagination token", function() {
"pagination token", function() {
let callCount = 0; let callCount = 0;
room.on("Room.timelineReset", function(emitRoom) { room.on(RoomEvent.TimelineReset, function(emitRoom) {
callCount += 1; callCount += 1;
expect(emitRoom).toEqual(room); expect(emitRoom).toEqual(room);
// make sure that the pagination token has been set before the // make sure that the pagination token has been set before the event is emitted.
// event is emitted. const tok = emitRoom?.getLiveTimeline().getPaginationToken(EventTimeline.BACKWARDS);
const tok = emitRoom.getLiveTimeline()
.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("pagToken"); expect(tok).toEqual("pagToken");
}); });
@ -693,7 +700,7 @@ describe("Room", function() {
const firstLiveTimeline = room.getLiveTimeline(); const firstLiveTimeline = room.getLiveTimeline();
room.resetLiveTimeline('sometoken', 'someothertoken'); room.resetLiveTimeline('sometoken', 'someothertoken');
const tl = room.getTimelineForEvent(events[0].getId()); const tl = room.getTimelineForEvent(events[0].getId()!);
expect(tl).toBe(timelineSupport ? firstLiveTimeline : null); expect(tl).toBe(timelineSupport ? firstLiveTimeline : null);
}); });
}; };
@ -721,30 +728,25 @@ describe("Room", function() {
it("should handle events in the same timeline", function() { it("should handle events in the same timeline", function() {
room.addLiveEvents(events); room.addLiveEvents(events);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!))
events[1].getId()))
.toBeLessThan(0); .toBeLessThan(0);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId()!, expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId()!, events[1].getId()!))
events[1].getId()))
.toBeGreaterThan(0); .toBeGreaterThan(0);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[1].getId()!))
events[1].getId()))
.toEqual(0); .toEqual(0);
}); });
it("should handle events in adjacent timelines", function() { it("should handle events in adjacent timelines", function() {
const oldTimeline = room.addTimeline(); const oldTimeline = room.addTimeline();
oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), 'f'); oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), Direction.Forward);
room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, 'b'); room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, Direction.Backward);
room.addEventsToTimeline([events[0]], false, oldTimeline); room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addLiveEvents([events[1]]); room.addLiveEvents([events[1]]);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!))
events[1].getId()))
.toBeLessThan(0); .toBeLessThan(0);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[0].getId()!))
events[0].getId()))
.toBeGreaterThan(0); .toBeGreaterThan(0);
}); });
@ -754,11 +756,9 @@ describe("Room", function() {
room.addEventsToTimeline([events[0]], false, oldTimeline); room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addLiveEvents([events[1]]); room.addLiveEvents([events[1]]);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!))
events[1].getId()))
.toBe(null); .toBe(null);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[0].getId()!))
events[0].getId()))
.toBe(null); .toBe(null);
}); });
@ -769,21 +769,21 @@ describe("Room", function() {
.compareEventOrdering(events[0].getId()!, "xxx")) .compareEventOrdering(events[0].getId()!, "xxx"))
.toBe(null); .toBe(null);
expect(room.getUnfilteredTimelineSet() expect(room.getUnfilteredTimelineSet()
.compareEventOrdering("xxx", events[0].getId())) .compareEventOrdering("xxx", events[0].getId()!))
.toBe(null); .toBe(null);
expect(room.getUnfilteredTimelineSet() expect(room.getUnfilteredTimelineSet()
.compareEventOrdering(events[0].getId()!, events[0].getId())) .compareEventOrdering(events[0].getId()!, events[0].getId()!))
.toBe(0); .toBe(0);
}); });
}); });
describe("getJoinedMembers", function() { describe("getJoinedMembers", function() {
it("should return members whose membership is 'join'", function() { it("should return members whose membership is 'join'", function() {
room.currentState.getMembers.mockImplementation(function() { mocked(room.currentState.getMembers).mockImplementation(function() {
return [ return [
{ userId: "@alice:bar", membership: "join" }, { userId: "@alice:bar", membership: "join" } as unknown as RoomMember,
{ userId: "@bob:bar", membership: "invite" }, { userId: "@bob:bar", membership: "invite" } as unknown as RoomMember,
{ userId: "@cleo:bar", membership: "leave" }, { userId: "@cleo:bar", membership: "leave" } as unknown as RoomMember,
]; ];
}); });
const res = room.getJoinedMembers(); const res = room.getJoinedMembers();
@ -792,9 +792,9 @@ describe("Room", function() {
}); });
it("should return an empty list if no membership is 'join'", function() { it("should return an empty list if no membership is 'join'", function() {
room.currentState.getMembers.mockImplementation(function() { mocked(room.currentState.getMembers).mockImplementation(function() {
return [ return [
{ userId: "@bob:bar", membership: "invite" }, { userId: "@bob:bar", membership: "invite" } as unknown as RoomMember,
]; ];
}); });
const res = room.getJoinedMembers(); const res = room.getJoinedMembers();
@ -805,41 +805,41 @@ describe("Room", function() {
describe("hasMembershipState", function() { describe("hasMembershipState", function() {
it("should return true for a matching userId and membership", it("should return true for a matching userId and membership",
function() { function() {
room.currentState.getMember.mockImplementation(function(userId) { mocked(room.currentState.getMember).mockImplementation(function(userId) {
return { return {
"@alice:bar": { userId: "@alice:bar", membership: "join" }, "@alice:bar": { userId: "@alice:bar", membership: "join" },
"@bob:bar": { userId: "@bob:bar", membership: "invite" }, "@bob:bar": { userId: "@bob:bar", membership: "invite" },
}[userId]; }[userId] as unknown as RoomMember;
}); });
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(true); expect(room.hasMembershipState("@bob:bar", "invite")).toBe(true);
}); });
it("should return false if match membership but no match userId", it("should return false if match membership but no match userId",
function() { function() {
room.currentState.getMember.mockImplementation(function(userId) { mocked(room.currentState.getMember).mockImplementation(function(userId) {
return { return {
"@alice:bar": { userId: "@alice:bar", membership: "join" }, "@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId]; }[userId] as unknown as RoomMember;
}); });
expect(room.hasMembershipState("@bob:bar", "join")).toBe(false); expect(room.hasMembershipState("@bob:bar", "join")).toBe(false);
}); });
it("should return false if match userId but no match membership", it("should return false if match userId but no match membership",
function() { function() {
room.currentState.getMember.mockImplementation(function(userId) { mocked(room.currentState.getMember).mockImplementation(function(userId) {
return { return {
"@alice:bar": { userId: "@alice:bar", membership: "join" }, "@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId]; }[userId] as unknown as RoomMember;
}); });
expect(room.hasMembershipState("@alice:bar", "ban")).toBe(false); expect(room.hasMembershipState("@alice:bar", "ban")).toBe(false);
}); });
it("should return false if no match membership or userId", it("should return false if no match membership or userId",
function() { function() {
room.currentState.getMember.mockImplementation(function(userId) { mocked(room.currentState.getMember).mockImplementation(function(userId) {
return { return {
"@alice:bar": { userId: "@alice:bar", membership: "join" }, "@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId]; }[userId] as unknown as RoomMember;
}); });
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(false); expect(room.hasMembershipState("@bob:bar", "invite")).toBe(false);
}); });
@ -1193,8 +1193,8 @@ describe("Room", function() {
event: true, event: true,
}); });
function mkReceipt(roomId: string, records) { function mkReceipt(roomId: string, records: Array<ReturnType<typeof mkRecord>>) {
const content = {}; const content: IContent = {};
records.forEach(function(r) { records.forEach(function(r) {
if (!content[r.eventId]) { if (!content[r.eventId]) {
content[r.eventId] = {}; content[r.eventId] = {};
@ -1241,7 +1241,7 @@ describe("Room", function() {
it("should emit an event when a receipt is added", it("should emit an event when a receipt is added",
function() { function() {
const listener = jest.fn(); const listener = jest.fn();
room.on("Room.receipt", listener); room.on(RoomEvent.Receipt, listener);
const ts = 13787898424; const ts = 13787898424;
@ -1448,7 +1448,7 @@ describe("Room", function() {
}); });
describe("tags", function() { describe("tags", function() {
function mkTags(roomId, tags) { function mkTags(roomId: string, tags: object) {
const content = { "tags": tags }; const content = { "tags": tags };
return new MatrixEvent({ return new MatrixEvent({
content: content, content: content,
@ -1470,7 +1470,7 @@ describe("Room", function() {
"received on the event stream", "received on the event stream",
function() { function() {
const listener = jest.fn(); const listener = jest.fn();
room.on("Room.tags", listener); room.on(RoomEvent.Tags, listener);
const tags = { "m.foo": { "order": 0.5 } }; const tags = { "m.foo": { "order": 0.5 } };
const event = mkTags(roomId, tags); const event = mkTags(roomId, tags);
@ -1642,11 +1642,14 @@ describe("Room", function() {
}); });
describe("loadMembersIfNeeded", function() { describe("loadMembersIfNeeded", function() {
function createClientMock(serverResponse, storageResponse: MatrixEvent[] | Error | null = null) { function createClientMock(
serverResponse: Error | MatrixEvent[],
storageResponse: MatrixEvent[] | Error | null = null,
) {
return { return {
getEventMapper: function() { getEventMapper: function() {
// events should already be MatrixEvents // events should already be MatrixEvents
return function(event) {return event;}; return function(event: MatrixEvent) {return event;};
}, },
isCryptoEnabled() { isCryptoEnabled() {
return true; return true;
@ -1671,7 +1674,7 @@ describe("Room", function() {
return Promise.resolve(this.storageResponse); return Promise.resolve(this.storageResponse);
} }
}, },
setOutOfBandMembers: function(roomId, memberEvents) { setOutOfBandMembers: function(roomId: string, memberEvents: IStateEventWithRoomId[]) {
this.storedMembers = memberEvents; this.storedMembers = memberEvents;
return Promise.resolve(); return Promise.resolve();
}, },
@ -2170,7 +2173,7 @@ describe("Room", function() {
}, },
}); });
room.createThread("$000", undefined, [eventWithoutARootEvent]); room.createThread("$000", undefined, [eventWithoutARootEvent], false);
const rootEvent = new MatrixEvent({ const rootEvent = new MatrixEvent({
event_id: "$666", event_id: "$666",
@ -2188,7 +2191,7 @@ describe("Room", function() {
}, },
}); });
expect(() => room.createThread(rootEvent.getId()!, rootEvent, [])).not.toThrow(); expect(() => room.createThread(rootEvent.getId()!, rootEvent, [], false)).not.toThrow();
}); });
it("creating thread from edited event should not conflate old versions of the event", () => { it("creating thread from edited event should not conflate old versions of the event", () => {
@ -2406,8 +2409,6 @@ describe("Room", function() {
Thread.setServerSideListSupport(FeatureSupport.Stable); Thread.setServerSideListSupport(FeatureSupport.Stable);
room.client.createThreadListMessagesRequest = () => Promise.resolve({ room.client.createThreadListMessagesRequest = () => Promise.resolve({
start: null,
end: null,
chunk: [], chunk: [],
state: [], state: [],
}); });
@ -2761,7 +2762,7 @@ describe("Room", function() {
}); });
describe("thread notifications", () => { describe("thread notifications", () => {
let room; let room: Room;
beforeEach(() => { beforeEach(() => {
const client = new TestClient(userA).client; const client = new TestClient(userA).client;

View File

@ -1,18 +1,19 @@
// This file had a function whose name is all caps, which displeases eslint // This file had a function whose name is all caps, which displeases eslint
/* eslint new-cap: "off" */ /* eslint new-cap: "off" */
import { defer } from '../../src/utils'; import { defer, IDeferred } from '../../src/utils';
import { MatrixError } from "../../src/http-api"; import { MatrixError } from "../../src/http-api";
import { MatrixScheduler } from "../../src/scheduler"; import { MatrixScheduler } from "../../src/scheduler";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { MatrixEvent } from "../../src";
jest.useFakeTimers(); jest.useFakeTimers();
describe("MatrixScheduler", function() { describe("MatrixScheduler", function() {
let scheduler; let scheduler: MatrixScheduler<Record<string, boolean>>;
let retryFn; let retryFn: Function | null;
let queueFn; let queueFn: ((event: MatrixEvent) => string | null) | null;
let deferred; let deferred: IDeferred<Record<string, boolean>>;
const roomId = "!foo:bar"; const roomId = "!foo:bar";
const eventA = utils.mkMessage({ const eventA = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true, user: "@alice:bar", room: roomId, event: true,
@ -65,8 +66,8 @@ describe("MatrixScheduler", function() {
deferB.resolve({ b: true }); deferB.resolve({ b: true });
deferA.resolve({ a: true }); deferA.resolve({ a: true });
const [a, b] = await abPromise; const [a, b] = await abPromise;
expect(a.a).toEqual(true); expect(a!.a).toEqual(true);
expect(b.b).toEqual(true); expect(b!.b).toEqual(true);
}); });
it("should invoke the retryFn on failure and wait the amount of time specified", it("should invoke the retryFn on failure and wait the amount of time specified",
@ -92,6 +93,7 @@ describe("MatrixScheduler", function() {
return new Promise(() => {}); return new Promise(() => {});
} }
expect(procCount).toBeLessThan(3); expect(procCount).toBeLessThan(3);
return new Promise(() => {});
}); });
scheduler.queueEvent(eventA); scheduler.queueEvent(eventA);
@ -119,8 +121,8 @@ describe("MatrixScheduler", function() {
return "yep"; return "yep";
}; };
const deferA = defer(); const deferA = defer<Record<string, boolean>>();
const deferB = defer(); const deferB = defer<Record<string, boolean>>();
let procCount = 0; let procCount = 0;
scheduler.setProcessFunction(function(ev) { scheduler.setProcessFunction(function(ev) {
procCount += 1; procCount += 1;
@ -132,6 +134,7 @@ describe("MatrixScheduler", function() {
return deferB.promise; return deferB.promise;
} }
expect(procCount).toBeLessThan(3); expect(procCount).toBeLessThan(3);
return new Promise<Record<string, boolean>>(() => {});
}); });
const globalA = scheduler.queueEvent(eventA); const globalA = scheduler.queueEvent(eventA);
@ -159,7 +162,7 @@ describe("MatrixScheduler", function() {
const eventC = utils.mkMessage({ user: "@a:bar", room: roomId, event: true }); const eventC = utils.mkMessage({ user: "@a:bar", room: roomId, event: true });
const eventD = utils.mkMessage({ user: "@b:bar", room: roomId, event: true }); const eventD = utils.mkMessage({ user: "@b:bar", room: roomId, event: true });
const buckets = {}; const buckets: Record<string, string> = {};
buckets[eventA.getId()!] = "queue_A"; buckets[eventA.getId()!] = "queue_A";
buckets[eventD.getId()!] = "queue_A"; buckets[eventD.getId()!] = "queue_A";
buckets[eventB.getId()!] = "queue_B"; buckets[eventB.getId()!] = "queue_B";
@ -169,13 +172,13 @@ describe("MatrixScheduler", function() {
return 0; return 0;
}; };
queueFn = function(event) { queueFn = function(event) {
return buckets[event.getId()]; return buckets[event.getId()!];
}; };
const expectOrder = [ const expectOrder = [
eventA.getId(), eventB.getId(), eventD.getId(), eventA.getId(), eventB.getId(), eventD.getId(),
]; ];
const deferA = defer<void>(); const deferA = defer<Record<string, boolean>>();
scheduler.setProcessFunction(function(event) { scheduler.setProcessFunction(function(event) {
const id = expectOrder.shift(); const id = expectOrder.shift();
expect(id).toEqual(event.getId()); expect(id).toEqual(event.getId());
@ -191,7 +194,7 @@ describe("MatrixScheduler", function() {
// wait a bit then resolve A and we should get D (not C) next. // wait a bit then resolve A and we should get D (not C) next.
setTimeout(function() { setTimeout(function() {
deferA.resolve(); deferA.resolve({});
}, 1000); }, 1000);
jest.advanceTimersByTime(1000); jest.advanceTimersByTime(1000);
}); });
@ -210,7 +213,7 @@ describe("MatrixScheduler", function() {
}; };
const prom = scheduler.queueEvent(eventA); const prom = scheduler.queueEvent(eventA);
expect(prom).toBeTruthy(); expect(prom).toBeTruthy();
expect(prom.then).toBeTruthy(); expect(prom!.then).toBeTruthy();
}); });
}); });
@ -237,15 +240,15 @@ describe("MatrixScheduler", function() {
scheduler.queueEvent(eventA); scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB); scheduler.queueEvent(eventB);
const queue = scheduler.getQueueForEvent(eventA); const queue = scheduler.getQueueForEvent(eventA);
expect(queue.length).toEqual(2); expect(queue).toHaveLength(2);
expect(queue).toEqual([eventA, eventB]); expect(queue).toEqual([eventA, eventB]);
// modify the queue // modify the queue
const eventC = utils.mkMessage( const eventC = utils.mkMessage(
{ user: "@a:bar", room: roomId, event: true }, { user: "@a:bar", room: roomId, event: true },
); );
queue.push(eventC); queue!.push(eventC);
const queueAgain = scheduler.getQueueForEvent(eventA); const queueAgain = scheduler.getQueueForEvent(eventA);
expect(queueAgain.length).toEqual(2); expect(queueAgain).toHaveLength(2);
}); });
it("should return a list of events in the queue and modifications to" + it("should return a list of events in the queue and modifications to" +
@ -255,10 +258,10 @@ describe("MatrixScheduler", function() {
}; };
scheduler.queueEvent(eventA); scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB); scheduler.queueEvent(eventB);
const queue = scheduler.getQueueForEvent(eventA); const queue = scheduler.getQueueForEvent(eventA)!;
queue[1].event.content.body = "foo"; queue[1].event.content!.body = "foo";
const queueAgain = scheduler.getQueueForEvent(eventA); const queueAgain = scheduler.getQueueForEvent(eventA)!;
expect(queueAgain[1].event.content.body).toEqual("foo"); expect(queueAgain[1].event.content?.body).toEqual("foo");
}); });
}); });

View File

@ -16,7 +16,8 @@ limitations under the License.
*/ */
import { ReceiptType } from "../../src/@types/read_receipts"; import { ReceiptType } from "../../src/@types/read_receipts";
import { SyncAccumulator } from "../../src/sync-accumulator"; import { IJoinedRoom, ISyncResponse, SyncAccumulator } from "../../src/sync-accumulator";
import { IRoomSummary } from "../../src";
// The event body & unsigned object get frozen to assert that they don't get altered // The event body & unsigned object get frozen to assert that they don't get altered
// by the impl // by the impl
@ -55,10 +56,10 @@ const RES_WITH_AGE = {
}, },
}, },
}, },
}; } as unknown as ISyncResponse;
describe("SyncAccumulator", function() { describe("SyncAccumulator", function() {
let sa; let sa: SyncAccumulator;
beforeEach(function() { beforeEach(function() {
sa = new SyncAccumulator({ sa = new SyncAccumulator({
@ -98,7 +99,7 @@ describe("SyncAccumulator", function() {
}, },
}, },
}, },
}; } as unknown as ISyncResponse;
sa.accumulate(res); sa.accumulate(res);
const output = sa.getJSON(); const output = sa.getJSON();
expect(output.nextBatch).toEqual(res.next_batch); expect(output.nextBatch).toEqual(res.next_batch);
@ -223,7 +224,6 @@ describe("SyncAccumulator", function() {
content: { content: {
user_ids: ["@alice:localhost"], user_ids: ["@alice:localhost"],
}, },
room_id: "!foo:bar",
}], }],
}, },
}); });
@ -281,12 +281,12 @@ describe("SyncAccumulator", function() {
account_data: { account_data: {
events: [acc1], events: [acc1],
}, },
}); } as unknown as ISyncResponse);
sa.accumulate({ sa.accumulate({
account_data: { account_data: {
events: [acc2], events: [acc2],
}, },
}); } as unknown as ISyncResponse);
expect( expect(
sa.getJSON().accountData.length, sa.getJSON().accountData.length,
).toEqual(1); ).toEqual(1);
@ -422,7 +422,7 @@ describe("SyncAccumulator", function() {
}); });
describe("summary field", function() { describe("summary field", function() {
function createSyncResponseWithSummary(summary) { function createSyncResponseWithSummary(summary: IRoomSummary): ISyncResponse {
return { return {
next_batch: "abc", next_batch: "abc",
rooms: { rooms: {
@ -444,7 +444,7 @@ describe("SyncAccumulator", function() {
}, },
}, },
}, },
}; } as unknown as ISyncResponse;
} }
afterEach(() => { afterEach(() => {
@ -487,8 +487,8 @@ describe("SyncAccumulator", function() {
jest.spyOn(global.Date, 'now').mockReturnValue(startingTs + delta); jest.spyOn(global.Date, 'now').mockReturnValue(startingTs + delta);
const output = sa.getJSON(); const output = sa.getJSON();
expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned.age).toEqual( expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned?.age).toEqual(
RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned.age + delta, RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned!.age! + delta,
); );
expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual( expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual(
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]), Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
@ -507,12 +507,12 @@ describe("SyncAccumulator", function() {
sa.accumulate(RES_WITH_AGE); sa.accumulate(RES_WITH_AGE);
const output = sa.getJSON(); const output = sa.getJSON();
expect(output.roomsData.join["!foo:bar"] expect(output.roomsData.join["!foo:bar"]
.unread_thread_notifications["$143273582443PhrSn:example.org"]).not.toBeUndefined(); .unread_thread_notifications!["$143273582443PhrSn:example.org"]).not.toBeUndefined();
}); });
}); });
}); });
function syncSkeleton(joinObj) { function syncSkeleton(joinObj: Partial<IJoinedRoom>): ISyncResponse {
joinObj = joinObj || {}; joinObj = joinObj || {};
return { return {
next_batch: "abc", next_batch: "abc",
@ -521,11 +521,12 @@ function syncSkeleton(joinObj) {
"!foo:bar": joinObj, "!foo:bar": joinObj,
}, },
}, },
}; } as unknown as ISyncResponse;
} }
function msg(localpart, text) { function msg(localpart: string, text: string) {
return { return {
event_id: "$" + Math.random(),
content: { content: {
body: text, body: text,
}, },
@ -535,8 +536,9 @@ function msg(localpart, text) {
}; };
} }
function member(localpart, membership) { function member(localpart: string, membership: string) {
return { return {
event_id: "$" + Math.random(),
content: { content: {
membership: membership, membership: membership,
}, },

View File

@ -455,7 +455,11 @@ describe("utils", function() {
describe("recursivelyAssign", () => { describe("recursivelyAssign", () => {
it("doesn't override with null/undefined", () => { it("doesn't override with null/undefined", () => {
const result = utils.recursivelyAssign( const result = utils.recursivelyAssign<{
string: string;
object: object;
float: number;
}, {}>(
{ {
string: "Hello world", string: "Hello world",
object: {}, object: {},
@ -475,7 +479,11 @@ describe("utils", function() {
}); });
it("assigns recursively", () => { it("assigns recursively", () => {
const result = utils.recursivelyAssign( const result = utils.recursivelyAssign<{
number: number;
object: object;
thing: string | object;
}, {}>(
{ {
number: 42, number: 42,
object: { object: {

View File

@ -933,7 +933,7 @@ describe('Call', function() {
await fakeIncomingCall(client, call, "1"); await fakeIncomingCall(client, call, "1");
}); });
const untilEventSent = async (...args) => { const untilEventSent = async (...args: any[]) => {
const maxTries = 20; const maxTries = 20;
for (let tries = 0; tries < maxTries; ++tries) { for (let tries = 0; tries < maxTries; ++tries) {
@ -971,7 +971,7 @@ describe('Call', function() {
}); });
describe("ICE candidate sending", () => { describe("ICE candidate sending", () => {
let mockPeerConn; let mockPeerConn: MockRTCPeerConnection;
const fakeCandidateString = "here is a fake candidate!"; const fakeCandidateString = "here is a fake candidate!";
const fakeCandidateEvent = { const fakeCandidateEvent = {
candidate: { candidate: {
@ -1086,7 +1086,7 @@ describe('Call', function() {
}); });
describe("Screen sharing", () => { describe("Screen sharing", () => {
const waitNegotiateFunc = resolve => { const waitNegotiateFunc = (resolve: Function): void => {
mockSendEvent.mockImplementationOnce(() => { mockSendEvent.mockImplementationOnce(() => {
// Note that the peer connection here is a dummy one and always returns // Note that the peer connection here is a dummy one and always returns
// dummy SDP, so there's not much point returning the content: the SDP will // dummy SDP, so there's not much point returning the content: the SDP will

View File

@ -42,7 +42,7 @@ export type ReceiptCache = {[eventId: string]: CachedReceipt[]};
export interface ReceiptContent { export interface ReceiptContent {
[eventId: string]: { [eventId: string]: {
[key in ReceiptType]: { [key in ReceiptType | string]: {
[userId: string]: Receipt; [userId: string]: Receipt;
}; };
}; };

View File

@ -49,7 +49,7 @@ interface WellKnownConfig extends Omit<IWellKnownConfig, "error"> {
error?: IWellKnownConfig["error"] | null; error?: IWellKnownConfig["error"] | null;
} }
interface ClientConfig { interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
"m.homeserver": WellKnownConfig; "m.homeserver": WellKnownConfig;
"m.identity_server": WellKnownConfig; "m.identity_server": WellKnownConfig;
} }
@ -131,7 +131,7 @@ export class AutoDiscovery {
* configuration, which may include error states. Rejects on unexpected * configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails. * failure, not when verification fails.
*/ */
public static async fromDiscoveryConfig(wellknown: any): Promise<ClientConfig> { public static async fromDiscoveryConfig(wellknown: IClientWellKnown): Promise<ClientConfig> {
// Step 1 is to get the config, which is provided to us here. // Step 1 is to get the config, which is provided to us here.
// We default to an error state to make the first few checks easier to // We default to an error state to make the first few checks easier to
@ -185,7 +185,7 @@ export class AutoDiscovery {
const hsVersions = await this.fetchWellKnownObject( const hsVersions = await this.fetchWellKnownObject(
`${hsUrl}/_matrix/client/versions`, `${hsUrl}/_matrix/client/versions`,
); );
if (!hsVersions || !hsVersions.raw["versions"]) { if (!hsVersions || !hsVersions.raw?.["versions"]) {
logger.error("Invalid /versions response"); logger.error("Invalid /versions response");
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER; clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
@ -219,9 +219,7 @@ export class AutoDiscovery {
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it // Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b. // points to an identity server in Step 5b.
isUrl = this.sanitizeWellKnownUrl( isUrl = this.sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]);
wellknown["m.identity_server"]["base_url"],
);
if (!isUrl) { if (!isUrl) {
logger.error("Invalid base_url for m.identity_server"); logger.error("Invalid base_url for m.identity_server");
failingClientConfig["m.identity_server"].error = failingClientConfig["m.identity_server"].error =
@ -234,7 +232,7 @@ export class AutoDiscovery {
const isResponse = await this.fetchWellKnownObject( const isResponse = await this.fetchWellKnownObject(
`${isUrl}/_matrix/identity/api/v1`, `${isUrl}/_matrix/identity/api/v1`,
); );
if (!isResponse || !isResponse.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) { if (!isResponse?.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
logger.error("Invalid /api/v1 response"); logger.error("Invalid /api/v1 response");
failingClientConfig["m.identity_server"].error = failingClientConfig["m.identity_server"].error =
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
@ -259,14 +257,16 @@ export class AutoDiscovery {
// Step 7: Copy any other keys directly into the clientConfig. This is for // Step 7: Copy any other keys directly into the clientConfig. This is for
// things like custom configuration of services. // things like custom configuration of services.
Object.keys(wellknown).forEach((k) => { Object.keys(wellknown).forEach((k: keyof IClientWellKnown) => {
if (k === "m.homeserver" || k === "m.identity_server") { if (k === "m.homeserver" || k === "m.identity_server") {
// Only copy selected parts of the config to avoid overwriting // Only copy selected parts of the config to avoid overwriting
// properties computed by the validation logic above. // properties computed by the validation logic above.
const notProps = ["error", "state", "base_url"]; const notProps = ["error", "state", "base_url"];
for (const prop of Object.keys(wellknown[k])) { for (const prop of Object.keys(wellknown[k]!)) {
if (notProps.includes(prop)) continue; if (notProps.includes(prop)) continue;
clientConfig[k][prop] = wellknown[k][prop]; type Prop = Exclude<keyof IWellKnownConfig, "error" | "state" | "base_url">;
// @ts-ignore - ts gets unhappy as we're mixing types here
clientConfig[k][prop as Prop] = wellknown[k]![prop as Prop];
} }
} else { } else {
// Just copy the whole thing over otherwise // Just copy the whole thing over otherwise
@ -347,7 +347,7 @@ export class AutoDiscovery {
} }
// Step 2: Validate and parse the config // Step 2: Validate and parse the config
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw); return AutoDiscovery.fromDiscoveryConfig(wellknown.raw!);
} }
/** /**
@ -378,7 +378,7 @@ export class AutoDiscovery {
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid. * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
* @private * @private
*/ */
private static sanitizeWellKnownUrl(url: string): string | false { private static sanitizeWellKnownUrl(url?: string | null): string | false {
if (!url) return false; if (!url) return false;
try { try {

View File

@ -16,27 +16,28 @@ limitations under the License.
import * as matrixcs from "./matrix"; import * as matrixcs from "./matrix";
type BrowserMatrix = typeof matrixcs;
declare global {
/* eslint-disable no-var, camelcase */
var __js_sdk_entrypoint: boolean;
var matrixcs: BrowserMatrix;
/* eslint-enable no-var */
}
if (global.__js_sdk_entrypoint) { if (global.__js_sdk_entrypoint) {
throw new Error("Multiple matrix-js-sdk entrypoints detected!"); throw new Error("Multiple matrix-js-sdk entrypoints detected!");
} }
global.__js_sdk_entrypoint = true; global.__js_sdk_entrypoint = true;
// just *accessing* indexedDB throws an exception in firefox with // just *accessing* indexedDB throws an exception in firefox with indexeddb disabled.
// indexeddb disabled. let indexedDB: IDBFactory | undefined;
let indexedDB;
try { try {
indexedDB = global.indexedDB; indexedDB = global.indexedDB;
} catch (e) {} } catch (e) {}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store. // if our browser (appears to) support indexeddb, use an indexeddb crypto store.
if (indexedDB) { if (indexedDB) {
matrixcs.setCryptoStoreFactory( matrixcs.setCryptoStoreFactory(() => new matrixcs.IndexedDBCryptoStore(indexedDB!, "matrix-js-sdk:crypto"));
function() {
return new matrixcs.IndexedDBCryptoStore(
indexedDB, "matrix-js-sdk:crypto",
);
},
);
} }
// We export 3 things to make browserify happy as well as downstream projects. // We export 3 things to make browserify happy as well as downstream projects.

View File

@ -530,7 +530,7 @@ export interface IPreviewUrlResponse {
"matrix:image:size"?: number; "matrix:image:size"?: number;
} }
interface ITurnServerResponse { export interface ITurnServerResponse {
uris: string[]; uris: string[];
username: string; username: string;
password: string; password: string;
@ -561,7 +561,7 @@ export interface IClientWellKnown {
} }
export interface IWellKnownConfig { export interface IWellKnownConfig {
raw?: any; // todo typings raw?: IClientWellKnown;
action?: AutoDiscoveryAction; action?: AutoDiscoveryAction;
reason?: string; reason?: string;
error?: Error | string; error?: Error | string;
@ -605,10 +605,10 @@ interface ITagMetadata {
} }
interface IMessagesResponse { interface IMessagesResponse {
start: string; start?: string;
end: string; end?: string;
chunk: IRoomEvent[]; chunk: IRoomEvent[];
state: IStateEvent[]; state?: IStateEvent[];
} }
interface IThreadedMessagesResponse { interface IThreadedMessagesResponse {
@ -635,6 +635,17 @@ export interface IUploadKeysRequest {
"org.matrix.msc2732.fallback_keys"?: Record<string, IOneTimeKey>; "org.matrix.msc2732.fallback_keys"?: Record<string, IOneTimeKey>;
} }
export interface IQueryKeysRequest {
device_keys: { [userId: string]: string[] };
timeout?: number;
token?: string;
}
export interface IClaimKeysRequest {
one_time_keys: { [userId: string]: { [deviceId: string]: string } };
timeout?: number;
}
export interface IOpenIDToken { export interface IOpenIDToken {
access_token: string; access_token: string;
token_type: "Bearer" | string; token_type: "Bearer" | string;
@ -1744,13 +1755,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
} }
return this.http.authedRequest<{ type Response = {
capabilities?: ICapabilities; capabilities?: ICapabilities;
}>(Method.Get, "/capabilities").catch((e: Error): void => { };
return this.http.authedRequest<Response>(Method.Get, "/capabilities").catch((e: Error): Response => {
// We swallow errors because we need a default object anyhow // We swallow errors because we need a default object anyhow
logger.error(e); logger.error(e);
return {};
}).then((r = {}) => { }).then((r = {}) => {
const capabilities: ICapabilities = r["capabilities"] || {}; const capabilities = r["capabilities"] || {};
// If the capabilities missed the cache, cache it for a shorter amount // If the capabilities missed the cache, cache it for a shorter amount
// of time to try and refresh them later. // of time to try and refresh them later.
@ -3229,28 +3242,28 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
targetRoomId: undefined, targetRoomId: undefined,
targetSessionId: undefined, targetSessionId: undefined,
backupInfo: IKeyBackupInfo, backupInfo: IKeyBackupInfo,
opts: IKeyBackupRestoreOpts, opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>; ): Promise<IKeyBackupRestoreResult>;
public restoreKeyBackupWithRecoveryKey( public restoreKeyBackupWithRecoveryKey(
recoveryKey: string, recoveryKey: string,
targetRoomId: string, targetRoomId: string,
targetSessionId: undefined, targetSessionId: undefined,
backupInfo: IKeyBackupInfo, backupInfo: IKeyBackupInfo,
opts: IKeyBackupRestoreOpts, opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>; ): Promise<IKeyBackupRestoreResult>;
public restoreKeyBackupWithRecoveryKey( public restoreKeyBackupWithRecoveryKey(
recoveryKey: string, recoveryKey: string,
targetRoomId: string, targetRoomId: string,
targetSessionId: string, targetSessionId: string,
backupInfo: IKeyBackupInfo, backupInfo: IKeyBackupInfo,
opts: IKeyBackupRestoreOpts, opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>; ): Promise<IKeyBackupRestoreResult>;
public restoreKeyBackupWithRecoveryKey( public restoreKeyBackupWithRecoveryKey(
recoveryKey: string, recoveryKey: string,
targetRoomId: string | undefined, targetRoomId: string | undefined,
targetSessionId: string | undefined, targetSessionId: string | undefined,
backupInfo: IKeyBackupInfo, backupInfo: IKeyBackupInfo,
opts: IKeyBackupRestoreOpts, opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult> { ): Promise<IKeyBackupRestoreResult> {
const privKey = decodeRecoveryKey(recoveryKey); const privKey = decodeRecoveryKey(recoveryKey);
return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts); return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts);
@ -3442,7 +3455,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
const deviceInfos = await this.crypto.downloadKeys(userIds); const deviceInfos = await this.crypto.downloadKeys(userIds);
const devicesByUser = {}; const devicesByUser: Record<string, DeviceInfo[]> = {};
for (const [userId, devices] of Object.entries(deviceInfos)) { for (const [userId, devices] of Object.entries(deviceInfos)) {
devicesByUser[userId] = Object.values(devices); devicesByUser[userId] = Object.values(devices);
} }
@ -3616,7 +3629,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public setIgnoredUsers(userIds: string[]): Promise<{}> { public setIgnoredUsers(userIds: string[]): Promise<{}> {
const content = { ignored_users: {} }; const content = { ignored_users: {} as Record<string, object> };
userIds.forEach((u) => { userIds.forEach((u) => {
content.ignored_users[u] = {}; content.ignored_users[u] = {};
}); });
@ -3910,16 +3923,25 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
): Promise<ISendEventResponse>; ): Promise<ISendEventResponse>;
public sendEvent( public sendEvent(
roomId: string, roomId: string,
threadId: string | null, threadIdOrEventType: string | null,
eventType: string | IContent, eventTypeOrContent: string | IContent,
content?: IContent | string, contentOrTxnId?: IContent | string,
txnId?: string, txnIdOrVoid?: string,
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { let threadId: string | null;
txnId = content as string; let eventType: string;
content = eventType as IContent; let content: IContent;
eventType = threadId; let txnId: string | undefined;
if (!threadIdOrEventType?.startsWith(EVENT_ID_PREFIX) && threadIdOrEventType !== null) {
txnId = contentOrTxnId as string;
content = eventTypeOrContent as IContent;
eventType = threadIdOrEventType;
threadId = null; threadId = null;
} else {
txnId = txnIdOrVoid;
content = contentOrTxnId as IContent;
eventType = eventTypeOrContent as string;
threadId = threadIdOrEventType;
} }
// If we expect that an event is part of a thread but is missing the relation // If we expect that an event is part of a thread but is missing the relation
@ -4280,7 +4302,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
let eventType: string = EventType.RoomMessage; let eventType: string = EventType.RoomMessage;
let sendContent: IContent = content as IContent; let sendContent: IContent = content as IContent;
const makeContentExtensible = (content: IContent = {}, recurse = true): IPartialEvent<object> | undefined => { const makeContentExtensible = (content: IContent = {}, recurse = true): IPartialEvent<object> | undefined => {
let newEvent: IPartialEvent<object> | undefined; let newEvent: IPartialEvent<IContent> | undefined;
if (content['msgtype'] === MsgType.Text) { if (content['msgtype'] === MsgType.Text) {
newEvent = MessageEvent.from(content['body'], content['formatted_body']).serialize(); newEvent = MessageEvent.from(content['body'], content['formatted_body']).serialize();
@ -5262,11 +5284,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline()); room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
this.processThreadEvents(room, threadedEvents, true); this.processThreadEvents(room, threadedEvents, true);
room.oldState.paginationToken = res.end; room.oldState.paginationToken = res.end ?? null;
if (res.chunk.length === 0) { if (res.chunk.length === 0) {
room.oldState.paginationToken = null; room.oldState.paginationToken = null;
} }
this.store.storeEvents(room, matrixEvents, res.end, true); this.store.storeEvents(room, matrixEvents, res.end ?? null, true);
delete this.ongoingScrollbacks[room.roomId]; delete this.ongoingScrollbacks[room.roomId];
resolve(room); resolve(room);
}).catch((err) => { }).catch((err) => {
@ -6261,7 +6283,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} roomId the id of the room. * @param {string} roomId the id of the room.
* @return {object} the rule or undefined. * @return {object} the rule or undefined.
*/ */
public getRoomPushRule(scope: string, roomId: string): IPushRule | undefined { public getRoomPushRule(scope: "global" | "device", roomId: string): IPushRule | undefined {
// There can be only room-kind push rule per room // There can be only room-kind push rule per room
// and its id is the room id. // and its id is the room id.
if (this.pushRules) { if (this.pushRules) {
@ -6282,7 +6304,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {Promise} Resolves: result object * @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public setRoomMutePushRule(scope: string, roomId: string, mute: boolean): Promise<void> | undefined { public setRoomMutePushRule(scope: "global" | "device", roomId: string, mute: boolean): Promise<void> | undefined {
let promise: Promise<unknown> | undefined; let promise: Promise<unknown> | undefined;
let hasDontNotifyRule = false; let hasDontNotifyRule = false;
@ -6818,7 +6840,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
.filter(([key, value]) => { .filter(([key, value]) => {
return primTypes.includes(typeof value); return primTypes.includes(typeof value);
}) })
.reduce((obj, [key, value]) => { .reduce<Record<string, any>>((obj, [key, value]) => {
obj[key] = value; obj[key] = value;
return obj; return obj;
}, {}); }, {});
@ -7841,7 +7863,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$roomId: roomId, $roomId: roomId,
}); });
const content = { const content: IContent = {
[ReceiptType.FullyRead]: rmEventId, [ReceiptType.FullyRead]: rmEventId,
[ReceiptType.Read]: rrEventId, [ReceiptType.Read]: rrEventId,
}; };
@ -8540,7 +8562,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* an error response ({@link module:http-api.MatrixError}). * an error response ({@link module:http-api.MatrixError}).
*/ */
public downloadKeysForUsers(userIds: string[], opts: { token?: string } = {}): Promise<IDownloadKeyResult> { public downloadKeysForUsers(userIds: string[], opts: { token?: string } = {}): Promise<IDownloadKeyResult> {
const content: any = { const content: IQueryKeysRequest = {
device_keys: {}, device_keys: {},
}; };
if ('token' in opts) { if ('token' in opts) {
@ -8582,7 +8604,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
queries[userId] = query; queries[userId] = query;
query[deviceId] = keyAlgorithm; query[deviceId] = keyAlgorithm;
} }
const content: any = { one_time_keys: queries }; const content: IClaimKeysRequest = { one_time_keys: queries };
if (timeout) { if (timeout) {
content.timeout = timeout; content.timeout = timeout;
} }
@ -8874,7 +8896,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const med = p[1].toLowerCase(); const med = p[1].toLowerCase();
const unhashed = `${addr} ${med}`; const unhashed = `${addr} ${med}`;
// Map the unhashed values to a known (case-sensitive) address. We use // Map the unhashed values to a known (case-sensitive) address. We use
// the case sensitive version because the caller might be expecting that. // the case-sensitive version because the caller might be expecting that.
localMapping[unhashed] = p[0]; localMapping[unhashed] = p[0];
return unhashed; return unhashed;
}); });
@ -8883,12 +8905,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw new Error("Unsupported identity server: unknown hash algorithm"); throw new Error("Unsupported identity server: unknown hash algorithm");
} }
const response = await this.http.idServerRequest( const response = await this.http.idServerRequest<{
Method.Post, "/lookup", mappings: { [address: string]: string };
params, IdentityPrefix.V2, identityAccessToken, }>(Method.Post, "/lookup", params, IdentityPrefix.V2, identityAccessToken);
);
if (!response || !response['mappings']) return []; // no results if (!response?.['mappings']) return []; // no results
const foundAddresses: { address: string, mxid: string }[] = []; const foundAddresses: { address: string, mxid: string }[] = [];
for (const hashed of Object.keys(response['mappings'])) { for (const hashed of Object.keys(response['mappings'])) {
@ -9028,7 +9049,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
messages: contentMap, messages: contentMap,
}; };
const targets = Object.keys(contentMap).reduce((obj, key) => { const targets = Object.keys(contentMap).reduce<Record<string, string[]>>((obj, key) => {
obj[key] = Object.keys(contentMap[key]); obj[key] = Object.keys(contentMap[key]);
return obj; return obj;
}, {}); }, {});

View File

@ -84,6 +84,7 @@ export class CrossSigningInfo {
const res = new CrossSigningInfo(userId); const res = new CrossSigningInfo(userId);
for (const prop in obj) { for (const prop in obj) {
if (obj.hasOwnProperty(prop)) { if (obj.hasOwnProperty(prop)) {
// @ts-ignore - ts doesn't like this and nor should we
res[prop] = obj[prop]; res[prop] = obj[prop];
} }
} }

View File

@ -35,7 +35,7 @@ import { IAccountDataClient } from "./SecretStorage";
interface ICrossSigningKeys { interface ICrossSigningKeys {
authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"]; authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
keys: Record<string, ICrossSigningKey>; keys: Record<"master" | "self_signing" | "user_signing", ICrossSigningKey>;
} }
/** /**
@ -209,7 +209,7 @@ export class EncryptionSetupOperation {
if (this.crossSigningKeys) { if (this.crossSigningKeys) {
const keys: Partial<CrossSigningKeys> = {}; const keys: Partial<CrossSigningKeys> = {};
for (const [name, key] of Object.entries(this.crossSigningKeys.keys)) { for (const [name, key] of Object.entries(this.crossSigningKeys.keys)) {
keys[name + "_key"] = key; keys[((name as keyof ICrossSigningKeys["keys"]) + "_key" as keyof CrossSigningKeys)] = key;
} }
// We must only call `uploadDeviceSigningKeys` from inside this auth // We must only call `uploadDeviceSigningKeys` from inside this auth

View File

@ -23,11 +23,19 @@ import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm"; import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
import { IMegolmSessionData } from "./index"; import { IMegolmSessionData } from "./index";
import { OlmGroupSessionExtraData } from "../@types/crypto"; import { OlmGroupSessionExtraData } from "../@types/crypto";
import { IMessage } from "./algorithms/olm";
// The maximum size of an event is 65K, and we base64 the content, so this is a // The maximum size of an event is 65K, and we base64 the content, so this is a
// reasonable approximation to the biggest plaintext we can encrypt. // reasonable approximation to the biggest plaintext we can encrypt.
const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4; const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
export class PayloadTooLargeError extends Error {
public readonly data = {
errcode: "M_TOO_LARGE",
error: "Payload too large for encrypted message",
};
}
function checkPayloadLength(payloadString: string): void { function checkPayloadLength(payloadString: string): void {
if (payloadString === undefined) { if (payloadString === undefined) {
throw new Error("payloadString undefined"); throw new Error("payloadString undefined");
@ -40,15 +48,8 @@ function checkPayloadLength(payloadString: string): void {
// Note that even if we manage to do the encryption, the message send may fail, // Note that even if we manage to do the encryption, the message send may fail,
// because by the time we've wrapped the ciphertext in the event object, it may // because by the time we've wrapped the ciphertext in the event object, it may
// exceed 65K. But at least we won't just fail with "abort()" in that case. // exceed 65K. But at least we won't just fail with "abort()" in that case.
const err = new Error("Message too long (" + payloadString.length + " bytes). " + throw new PayloadTooLargeError(`Message too long (${payloadString.length} bytes). ` +
"The maximum for an encrypted message is " + `The maximum for an encrypted message is ${MAX_PLAINTEXT_LENGTH} bytes.`);
MAX_PLAINTEXT_LENGTH + " bytes.");
// TODO: [TypeScript] We should have our own error types
err["data"] = {
errcode: "M_TOO_LARGE",
error: "Payload too large for encrypted message",
};
throw err;
} }
} }
@ -126,6 +127,8 @@ interface IInboundGroupSessionKey {
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
type OneTimeKeys = { curve25519: { [keyId: string]: string } };
/** /**
* Manages the olm cryptography functions. Each OlmDevice has a single * Manages the olm cryptography functions. Each OlmDevice has a single
* OlmAccount and a number of OlmSessions. * OlmAccount and a number of OlmSessions.
@ -452,7 +455,7 @@ export class OlmDevice {
* @return {Promise<string>} base64-encoded signature * @return {Promise<string>} base64-encoded signature
*/ */
public async sign(message: string): Promise<string> { public async sign(message: string): Promise<string> {
let result; let result: string;
await this.cryptoStore.doTxn( await this.cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => { (txn) => {
@ -460,7 +463,7 @@ export class OlmDevice {
result = account.sign(message); result = account.sign(message);
}); });
}); });
return result; return result!;
} }
/** /**
@ -470,8 +473,8 @@ export class OlmDevice {
* <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519 * <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
* key. * key.
*/ */
public async getOneTimeKeys(): Promise<{ curve25519: { [keyId: string]: string } }> { public async getOneTimeKeys(): Promise<OneTimeKeys> {
let result; let result: OneTimeKeys;
await this.cryptoStore.doTxn( await this.cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => { (txn) => {
@ -481,7 +484,7 @@ export class OlmDevice {
}, },
); );
return result; return result!;
} }
/** /**
@ -821,10 +824,10 @@ export class OlmDevice {
theirDeviceIdentityKey: string, theirDeviceIdentityKey: string,
sessionId: string, sessionId: string,
payloadString: string, payloadString: string,
): Promise<string> { ): Promise<IMessage> {
checkPayloadLength(payloadString); checkPayloadLength(payloadString);
let res; let res: IMessage;
await this.cryptoStore.doTxn( await this.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => { (txn) => {
@ -840,7 +843,7 @@ export class OlmDevice {
}, },
logger.withPrefix("[encryptMessage]"), logger.withPrefix("[encryptMessage]"),
); );
return res; return res!;
} }
/** /**
@ -860,7 +863,7 @@ export class OlmDevice {
messageType: number, messageType: number,
ciphertext: string, ciphertext: string,
): Promise<string> { ): Promise<string> {
let payloadString; let payloadString: string;
await this.cryptoStore.doTxn( await this.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => { (txn) => {
@ -877,7 +880,7 @@ export class OlmDevice {
}, },
logger.withPrefix("[decryptMessage]"), logger.withPrefix("[decryptMessage]"),
); );
return payloadString; return payloadString!;
} }
/** /**
@ -902,7 +905,7 @@ export class OlmDevice {
return false; return false;
} }
let matches; let matches: boolean;
await this.cryptoStore.doTxn( await this.cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_SESSIONS], 'readonly', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => { (txn) => {
@ -912,7 +915,7 @@ export class OlmDevice {
}, },
logger.withPrefix("[matchesSession]"), logger.withPrefix("[matchesSession]"),
); );
return matches; return matches!;
} }
public async recordSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> { public async recordSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
@ -1293,7 +1296,7 @@ export class OlmDevice {
result = null; result = null;
return; return;
} }
let res; let res: ReturnType<InboundGroupSession["decrypt"]>;
try { try {
res = session.decrypt(body); res = session.decrypt(body);
} catch (e) { } catch (e) {
@ -1313,8 +1316,8 @@ export class OlmDevice {
let plaintext: string = res.plaintext; let plaintext: string = res.plaintext;
if (plaintext === undefined) { if (plaintext === undefined) {
// Compatibility for older olm versions. // @ts-ignore - Compatibility for older olm versions.
plaintext = res; plaintext = res as string;
} else { } else {
// Check if we have seen this message index before to detect replay attacks. // Check if we have seen this message index before to detect replay attacks.
// If the event ID and timestamp are specified, and the match the event ID // If the event ID and timestamp are specified, and the match the event ID
@ -1555,7 +1558,7 @@ export class OlmDevice {
} }
} }
export const WITHHELD_MESSAGES = { export const WITHHELD_MESSAGES: Record<string, string> = {
"m.unverified": "The sender has disabled encrypting to unverified devices.", "m.unverified": "The sender has disabled encrypting to unverified devices.",
"m.blacklisted": "The sender has blocked you.", "m.blacklisted": "The sender has blocked you.",
"m.unauthorised": "You are not authorised to read the message.", "m.unauthorised": "You are not authorised to read the message.",

View File

@ -18,10 +18,9 @@ import { v4 as uuidv4 } from 'uuid';
import { logger } from '../logger'; import { logger } from '../logger';
import * as olmlib from './olmlib'; import * as olmlib from './olmlib';
import { encodeBase64 } from './olmlib';
import { randomString } from '../randomstring'; import { randomString } from '../randomstring';
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes'; import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
import { ICryptoCallbacks } from "."; import { ICryptoCallbacks, IEncryptedContent } from ".";
import { IContent, MatrixEvent } from "../models/event"; import { IContent, MatrixEvent } from "../models/event";
import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client"; import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client";
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api'; import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
@ -61,8 +60,7 @@ interface IDecryptors {
interface ISecretInfo { interface ISecretInfo {
encrypted: { encrypted: {
// eslint-disable-next-line camelcase [keyId: string]: IEncryptedPayload;
key_id: IEncryptedPayload;
}; };
} }
@ -318,23 +316,11 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
`the keys it is encrypted with are for a supported algorithm`); `the keys it is encrypted with are for a supported algorithm`);
} }
let keyId: string; // fetch private key from app
let decryption; const [keyId, decryption] = await this.getSecretStorageKey(keys, name);
try { const encInfo = secretInfo.encrypted[keyId];
// fetch private key from app
[keyId, decryption] = await this.getSecretStorageKey(keys, name);
const encInfo = secretInfo.encrypted[keyId]; return decryption.decrypt(encInfo);
// We don't actually need the decryption object if it's a passthrough
// since we just want to return the key itself. It must be base64
// encoded, since this is how a key would normally be stored.
if (encInfo.passthrough) return encodeBase64(decryption.get_private_key());
return decryption.decrypt(encInfo);
} finally {
if (decryption && decryption.free) decryption.free();
}
} }
/** /**
@ -351,7 +337,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name); const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo?.encrypted) return null; if (!secretInfo?.encrypted) return null;
const ret = {}; const ret: Record<string, ISecretStorageKeyInfo> = {};
// filter secret encryption keys with supported algorithm // filter secret encryption keys with supported algorithm
for (const keyId of Object.keys(secretInfo.encrypted)) { for (const keyId of Object.keys(secretInfo.encrypted)) {
@ -391,7 +377,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
requesting_device_id: this.baseApis.deviceId, requesting_device_id: this.baseApis.deviceId,
request_id: requestId, request_id: requestId,
}; };
const toDevice = {}; const toDevice: Record<string, typeof cancelData> = {};
for (const device of devices) { for (const device of devices) {
toDevice[device] = cancelData; toDevice[device] = cancelData;
} }
@ -412,7 +398,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
request_id: requestId, request_id: requestId,
[ToDeviceMessageId]: uuidv4(), [ToDeviceMessageId]: uuidv4(),
}; };
const toDevice = {}; const toDevice: Record<string, typeof requestData> = {};
for (const device of devices) { for (const device of devices) {
toDevice[device] = requestData; toDevice[device] = requestData;
} }
@ -490,9 +476,9 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
secret: secret, secret: secret,
}, },
}; };
const encryptedContent = { const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM, algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key, sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key!,
ciphertext: {}, ciphertext: {},
[ToDeviceMessageId]: uuidv4(), [ToDeviceMessageId]: uuidv4(),
}; };

View File

@ -23,8 +23,14 @@ limitations under the License.
import { MatrixClient } from "../../client"; import { MatrixClient } from "../../client";
import { Room } from "../../models/room"; import { Room } from "../../models/room";
import { OlmDevice } from "../OlmDevice"; import { OlmDevice } from "../OlmDevice";
import { MatrixEvent, RoomMember } from "../../matrix"; import { IContent, MatrixEvent, RoomMember } from "../../matrix";
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from ".."; import {
Crypto,
IEncryptedContent,
IEventDecryptionResult,
IMegolmSessionData,
IncomingRoomKeyRequest,
} from "..";
import { DeviceInfo } from "../deviceinfo"; import { DeviceInfo } from "../deviceinfo";
import { IRoomEncryption } from "../RoomList"; import { IRoomEncryption } from "../RoomList";
@ -108,7 +114,7 @@ export abstract class EncryptionAlgorithm {
* *
* @return {Promise} Promise which resolves to the new event body * @return {Promise} Promise which resolves to the new event body
*/ */
public abstract encryptMessage(room: Room, eventType: string, content: object): Promise<object>; public abstract encryptMessage(room: Room, eventType: string, content: IContent): Promise<IEncryptedContent>;
/** /**
* Called when the membership of a member of the room changes. * Called when the membership of a member of the room changes.

View File

@ -38,9 +38,15 @@ import { Room } from '../../models/room';
import { DeviceInfo } from "../deviceinfo"; import { DeviceInfo } from "../deviceinfo";
import { IOlmSessionResult } from "../olmlib"; import { IOlmSessionResult } from "../olmlib";
import { DeviceInfoMap } from "../DeviceList"; import { DeviceInfoMap } from "../DeviceList";
import { MatrixEvent } from "../../models/event"; import { IContent, MatrixEvent } from "../../models/event";
import { EventType, MsgType, ToDeviceMessageId } from '../../@types/event'; import { EventType, MsgType, ToDeviceMessageId } from '../../@types/event';
import { IEncryptedContent, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index"; import {
IMegolmEncryptedContent,
IEventDecryptionResult,
IMegolmSessionData,
IncomingRoomKeyRequest,
IEncryptedContent,
} from "../index";
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager'; import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
import { OlmGroupSessionExtraData } from "../../@types/crypto"; import { OlmGroupSessionExtraData } from "../../@types/crypto";
import { MatrixError } from "../../http-api"; import { MatrixError } from "../../http-api";
@ -228,7 +234,7 @@ class OutboundSessionInfo {
* @param {object} params parameters, as per * @param {object} params parameters, as per
* {@link module:crypto/algorithms/EncryptionAlgorithm} * {@link module:crypto/algorithms/EncryptionAlgorithm}
*/ */
class MegolmEncryption extends EncryptionAlgorithm { export class MegolmEncryption extends EncryptionAlgorithm {
// the most recent attempt to set up a session. This is used to serialise // the most recent attempt to set up a session. This is used to serialise
// the session setups, so that we have a race-free view of which session we // the session setups, so that we have a race-free view of which session we
// are using, and which devices we have shared the keys with. It resolves // are using, and which devices we have shared the keys with. It resolves
@ -761,9 +767,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
}, },
}; };
const encryptedContent = { const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM, algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key, sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {}, ciphertext: {},
[ToDeviceMessageId]: uuidv4(), [ToDeviceMessageId]: uuidv4(),
}; };
@ -1010,7 +1016,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
* *
* @return {Promise} Promise which resolves to the new event body * @return {Promise} Promise which resolves to the new event body
*/ */
public async encryptMessage(room: Room, eventType: string, content: object): Promise<object> { public async encryptMessage(room: Room, eventType: string, content: IContent): Promise<IMegolmEncryptedContent> {
logger.log(`Starting to encrypt event for ${this.roomId}`); logger.log(`Starting to encrypt event for ${this.roomId}`);
if (this.encryptionPreparation != null) { if (this.encryptionPreparation != null) {
@ -1045,12 +1051,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
content: content, content: content,
}; };
const ciphertext = this.olmDevice.encryptGroupMessage( const ciphertext = this.olmDevice.encryptGroupMessage(session.sessionId, JSON.stringify(payloadJson));
session.sessionId, JSON.stringify(payloadJson), const encryptedContent: IEncryptedContent = {
);
const encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM, algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key, sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: ciphertext, ciphertext: ciphertext,
session_id: session.sessionId, session_id: session.sessionId,
// Include our device ID so that recipients can send us a // Include our device ID so that recipients can send us a
@ -1064,7 +1068,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
return encryptedContent; return encryptedContent;
} }
private isVerificationEvent(eventType: string, content: object): boolean { private isVerificationEvent(eventType: string, content: IContent): boolean {
switch (eventType) { switch (eventType) {
case EventType.KeyVerificationCancel: case EventType.KeyVerificationCancel:
case EventType.KeyVerificationDone: case EventType.KeyVerificationDone:
@ -1227,7 +1231,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
* @param {object} params parameters, as per * @param {object} params parameters, as per
* {@link module:crypto/algorithms/DecryptionAlgorithm} * {@link module:crypto/algorithms/DecryptionAlgorithm}
*/ */
class MegolmDecryption extends DecryptionAlgorithm { export class MegolmDecryption extends DecryptionAlgorithm {
// events which we couldn't decrypt due to unknown sessions / // events which we couldn't decrypt due to unknown sessions /
// indexes, or which we could only decrypt with untrusted keys: // indexes, or which we could only decrypt with untrusted keys:
// map from senderKey|sessionId to Set of MatrixEvents // map from senderKey|sessionId to Set of MatrixEvents
@ -1670,9 +1674,9 @@ class MegolmDecryption extends DecryptionAlgorithm {
await olmlib.ensureOlmSessionsForDevices( await olmlib.ensureOlmSessionsForDevices(
this.olmDevice, this.baseApis, { [sender]: [device] }, false, this.olmDevice, this.baseApis, { [sender]: [device] }, false,
); );
const encryptedContent = { const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM, algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key, sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {}, ciphertext: {},
[ToDeviceMessageId]: uuidv4(), [ToDeviceMessageId]: uuidv4(),
}; };
@ -1752,9 +1756,9 @@ class MegolmDecryption extends DecryptionAlgorithm {
body.room_id, body.sender_key, body.session_id, body.room_id, body.sender_key, body.session_id,
); );
}).then((payload) => { }).then((payload) => {
const encryptedContent = { const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM, algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key, sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {}, ciphertext: {},
[ToDeviceMessageId]: uuidv4(), [ToDeviceMessageId]: uuidv4(),
}; };

View File

@ -30,13 +30,13 @@ import {
registerAlgorithm, registerAlgorithm,
} from "./base"; } from "./base";
import { Room } from '../../models/room'; import { Room } from '../../models/room';
import { MatrixEvent } from "../../models/event"; import { IContent, MatrixEvent } from "../../models/event";
import { IEventDecryptionResult } from "../index"; import { IEncryptedContent, IEventDecryptionResult, IOlmEncryptedContent } from "../index";
import { IInboundSession } from "../OlmDevice"; import { IInboundSession } from "../OlmDevice";
const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceVerification = DeviceInfo.DeviceVerification;
interface IMessage { export interface IMessage {
type: number; type: number;
body: string; body: string;
} }
@ -91,7 +91,7 @@ class OlmEncryption extends EncryptionAlgorithm {
* *
* @return {Promise} Promise which resolves to the new event body * @return {Promise} Promise which resolves to the new event body
*/ */
public async encryptMessage(room: Room, eventType: string, content: object): Promise<object> { public async encryptMessage(room: Room, eventType: string, content: IContent): Promise<IOlmEncryptedContent> {
// pick the list of recipients based on the membership list. // pick the list of recipients based on the membership list.
// //
// TODO: there is a race condition here! What if a new user turns up // TODO: there is a race condition here! What if a new user turns up
@ -111,9 +111,9 @@ class OlmEncryption extends EncryptionAlgorithm {
content: content, content: content,
}; };
const encryptedContent = { const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM, algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key, sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {}, ciphertext: {},
}; };

View File

@ -93,7 +93,7 @@ interface BackupAlgorithmClass {
interface BackupAlgorithm { interface BackupAlgorithm {
untrusted: boolean; untrusted: boolean;
encryptSession(data: Record<string, any>): Promise<any>; encryptSession(data: Record<string, any>): Promise<Curve25519SessionData | IEncryptedPayload>;
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>; decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
authData: AuthData; authData: AuthData;
keyMatches(key: ArrayLike<number>): Promise<boolean>; keyMatches(key: ArrayLike<number>): Promise<boolean>;
@ -663,7 +663,7 @@ export class Curve25519 implements BackupAlgorithm {
public get untrusted(): boolean { return true; } public get untrusted(): boolean { return true; }
public async encryptSession(data: Record<string, any>): Promise<any> { public async encryptSession(data: Record<string, any>): Promise<Curve25519SessionData> {
const plainText: Record<string, any> = Object.assign({}, data); const plainText: Record<string, any> = Object.assign({}, data);
delete plainText.session_id; delete plainText.session_id;
delete plainText.room_id; delete plainText.room_id;
@ -788,7 +788,7 @@ export class Aes256 implements BackupAlgorithm {
public get untrusted(): boolean { return false; } public get untrusted(): boolean { return false; }
public encryptSession(data: Record<string, any>): Promise<any> { public encryptSession(data: Record<string, any>): Promise<IEncryptedPayload> {
const plainText: Record<string, any> = Object.assign({}, data); const plainText: Record<string, any> = Object.assign({}, data);
delete plainText.session_id; delete plainText.session_id;
delete plainText.room_id; delete plainText.room_id;

View File

@ -258,7 +258,7 @@ export class DehydrationManager {
} }
logger.log("Preparing fallback keys"); logger.log("Preparing fallback keys");
const fallbackKeys = {}; const fallbackKeys: Record<string, IOneTimeKey> = {};
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) { for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
const k: IOneTimeKey = { key, fallback: true }; const k: IOneTimeKey = { key, fallback: true };
const signature = account.sign(anotherjson.stringify(k)); const signature = account.sign(anotherjson.stringify(k));

View File

@ -72,7 +72,8 @@ export class DeviceInfo {
const res = new DeviceInfo(deviceId); const res = new DeviceInfo(deviceId);
for (const prop in obj) { for (const prop in obj) {
if (obj.hasOwnProperty(prop)) { if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop]; // @ts-ignore - this is messy and typescript doesn't like it
res[prop as keyof IDevice] = obj[prop as keyof IDevice];
} }
} }
return res; return res;

View File

@ -88,6 +88,9 @@ import { CryptoStore } from "./store/base";
import { IVerificationChannel } from "./verification/request/Channel"; import { IVerificationChannel } from "./verification/request/Channel";
import { TypedEventEmitter } from "../models/typed-event-emitter"; import { TypedEventEmitter } from "../models/typed-event-emitter";
import { IContent } from "../models/event"; import { IContent } from "../models/event";
import { ISyncResponse } from "../sync-accumulator";
import { ISignatures } from "../@types/signed";
import { IMessage } from "./algorithms/olm";
const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceVerification = DeviceInfo.DeviceVerification;
@ -100,7 +103,7 @@ const defaultVerificationMethods = {
// to start. // to start.
[SHOW_QR_CODE_METHOD]: IllegalMethod, [SHOW_QR_CODE_METHOD]: IllegalMethod,
[SCAN_QR_CODE_METHOD]: IllegalMethod, [SCAN_QR_CODE_METHOD]: IllegalMethod,
}; } as const;
/** /**
* verification method names * verification method names
@ -109,7 +112,7 @@ const defaultVerificationMethods = {
export const verificationMethods = { export const verificationMethods = {
RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME, RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
SAS: SASVerification.NAME, SAS: SASVerification.NAME,
}; } as const;
export type VerificationMethod = keyof typeof verificationMethods | string; export type VerificationMethod = keyof typeof verificationMethods | string;
@ -200,18 +203,13 @@ interface IUserOlmSession {
}[]; }[];
} }
interface ISyncDeviceLists {
changed: string[];
left: string[];
}
export interface IRoomKeyRequestRecipient { export interface IRoomKeyRequestRecipient {
userId: string; userId: string;
deviceId: string; deviceId: string;
} }
interface ISignableObject { interface ISignableObject {
signatures?: object; signatures?: ISignatures;
unsigned?: object; unsigned?: object;
} }
@ -231,14 +229,25 @@ export interface IRequestsMap {
} }
/* eslint-disable camelcase */ /* eslint-disable camelcase */
export interface IEncryptedContent { export interface IOlmEncryptedContent {
algorithm: string; algorithm: typeof olmlib.OLM_ALGORITHM;
sender_key: string; sender_key: string;
ciphertext: Record<string, string>; ciphertext: Record<string, IMessage>;
[ToDeviceMessageId]: string; [ToDeviceMessageId]?: string;
}
export interface IMegolmEncryptedContent {
algorithm: typeof olmlib.MEGOLM_ALGORITHM;
sender_key: string;
session_id: string;
device_id: string;
ciphertext: string;
[ToDeviceMessageId]?: string;
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
export type IEncryptedContent = IOlmEncryptedContent | IMegolmEncryptedContent;
export enum CryptoEvent { export enum CryptoEvent {
DeviceVerificationChanged = "deviceVerificationChanged", DeviceVerificationChanged = "deviceVerificationChanged",
UserTrustStatusChanged = "userTrustStatusChanged", UserTrustStatusChanged = "userTrustStatusChanged",
@ -382,7 +391,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
private readonly clientStore: IStore, private readonly clientStore: IStore,
public readonly cryptoStore: CryptoStore, public readonly cryptoStore: CryptoStore,
private readonly roomList: RoomList, private readonly roomList: RoomList,
verificationMethods: Array<keyof typeof defaultVerificationMethods | typeof VerificationBase>, verificationMethods: Array<VerificationMethod | (typeof VerificationBase & { NAME: string })>,
) { ) {
super(); super();
this.reEmitter = new TypedReEmitter(this); this.reEmitter = new TypedReEmitter(this);
@ -2947,7 +2956,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @param {Object} syncDeviceLists device_lists field from /sync, or response from * @param {Object} syncDeviceLists device_lists field from /sync, or response from
* /keys/changes * /keys/changes
*/ */
public async handleDeviceListChanges(syncData: ISyncStateData, syncDeviceLists: ISyncDeviceLists): Promise<void> { public async handleDeviceListChanges(
syncData: ISyncStateData,
syncDeviceLists: Required<ISyncResponse>["device_lists"],
): Promise<void> {
// Initial syncs don't have device change lists. We'll either get the complete list // Initial syncs don't have device change lists. We'll either get the complete list
// of changes for the interval or will have invalidated everything in willProcessSync // of changes for the interval or will have invalidated everything in willProcessSync
if (!syncData.oldSyncToken) return; if (!syncData.oldSyncToken) return;
@ -3087,15 +3099,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @param {Object} deviceLists device_lists field from /sync, or response from * @param {Object} deviceLists device_lists field from /sync, or response from
* /keys/changes * /keys/changes
*/ */
private async evalDeviceListChanges(deviceLists: ISyncDeviceLists): Promise<void> { private async evalDeviceListChanges(deviceLists: Required<ISyncResponse>["device_lists"]): Promise<void> {
if (deviceLists.changed && Array.isArray(deviceLists.changed)) { if (Array.isArray(deviceLists?.changed)) {
deviceLists.changed.forEach((u) => { deviceLists.changed.forEach((u) => {
this.deviceList.invalidateUserDeviceList(u); this.deviceList.invalidateUserDeviceList(u);
}); });
} }
if (deviceLists.left && Array.isArray(deviceLists.left) && if (Array.isArray(deviceLists?.left) && deviceLists.left.length) {
deviceLists.left.length) {
// Check we really don't share any rooms with these users // Check we really don't share any rooms with these users
// any more: the server isn't required to give us the // any more: the server isn't required to give us the
// exact correct set. // exact correct set.
@ -3515,9 +3526,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// same order we sent them, the other end will get this first, set up the new session, // same order we sent them, the other end will get this first, set up the new session,
// then get the keyshare request and send the key over this new session (because it // then get the keyshare request and send the key over this new session (because it
// is the session it has most recently received a message on). // is the session it has most recently received a message on).
const encryptedContent = { const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM, algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key, sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {}, ciphertext: {},
[ToDeviceMessageId]: uuidv4(), [ToDeviceMessageId]: uuidv4(),
}; };
@ -3842,7 +3853,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* *
* @param {Object} obj Object to which we will add a 'signatures' property * @param {Object} obj Object to which we will add a 'signatures' property
*/ */
public async signObject(obj: object & ISignableObject): Promise<void> { public async signObject<T extends ISignableObject & object>(obj: T): Promise<void> {
const sigs = obj.signatures || {}; const sigs = obj.signatures || {};
const unsigned = obj.unsigned; const unsigned = obj.unsigned;

View File

@ -31,6 +31,7 @@ import { IClaimOTKsResult, MatrixClient } from "../client";
import { ISignatures } from "../@types/signed"; import { ISignatures } from "../@types/signed";
import { MatrixEvent } from "../models/event"; import { MatrixEvent } from "../models/event";
import { EventType } from "../@types/event"; import { EventType } from "../@types/event";
import { IMessage } from "./algorithms/olm";
enum Algorithm { enum Algorithm {
Olm = "m.olm.v1.curve25519-aes-sha2", Olm = "m.olm.v1.curve25519-aes-sha2",
@ -75,7 +76,7 @@ export interface IOlmSessionResult {
* has been encrypted into `resultsObject` * has been encrypted into `resultsObject`
*/ */
export async function encryptMessageForDevice( export async function encryptMessageForDevice(
resultsObject: Record<string, string>, resultsObject: Record<string, IMessage>,
ourUserId: string, ourUserId: string,
ourDeviceId: string | undefined, ourDeviceId: string | undefined,
olmDevice: OlmDevice, olmDevice: OlmDevice,
@ -124,6 +125,7 @@ export async function encryptMessageForDevice(
recipient_keys: { recipient_keys: {
"ed25519": recipientDevice.getFingerprint(), "ed25519": recipientDevice.getFingerprint(),
}, },
...payloadFields,
}; };
// TODO: technically, a bunch of that stuff only needs to be included for // TODO: technically, a bunch of that stuff only needs to be included for
@ -131,11 +133,7 @@ export async function encryptMessageForDevice(
// involved in the session. If we're looking to reduce data transfer in the // involved in the session. If we're looking to reduce data transfer in the
// future, we could elide them for subsequent messages. // future, we could elide them for subsequent messages.
Object.assign(payload, payloadFields); resultsObject[deviceKey] = await olmDevice.encryptMessage(deviceKey, sessionId, JSON.stringify(payload));
resultsObject[deviceKey] = await olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload),
);
} }
interface IExistingOlmSession { interface IExistingOlmSession {
@ -495,7 +493,7 @@ export async function verifySignature(
* @param {string} pubKey The public key (ignored if key is a seed) * @param {string} pubKey The public key (ignored if key is a seed)
* @returns {string} the signature for the object * @returns {string} the signature for the object
*/ */
export function pkSign(obj: IObject, key: PkSigning, userId: string, pubKey: string): string { export function pkSign(obj: object & IObject, key: Uint8Array | PkSigning, userId: string, pubKey: string): string {
let createdKey = false; let createdKey = false;
if (key instanceof Uint8Array) { if (key instanceof Uint8Array) {
const keyObj = new global.Olm.PkSigning(); const keyObj = new global.Olm.PkSigning();

View File

@ -69,7 +69,7 @@ export interface CryptoStore {
deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise<OutgoingRoomKeyRequest | null>; deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise<OutgoingRoomKeyRequest | null>;
// Olm Account // Olm Account
getAccount(txn: unknown, func: (accountPickle: string | null) => void); getAccount(txn: unknown, func: (accountPickle: string | null) => void): void;
storeAccount(txn: unknown, accountPickle: string): void; storeAccount(txn: unknown, accountPickle: string): void;
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void; getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void;
getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>( getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(

View File

@ -338,20 +338,20 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
} }
public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> { public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
const sessionsNeedingBackup const sessionsNeedingBackup = getJsonItem<{
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; [senderKeySessionId: string]: string;
}>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) { for (const session of sessions) {
delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId]; delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId];
} }
setJsonItem( setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup);
this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup,
);
return Promise.resolve(); return Promise.resolve();
} }
public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> { public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
const sessionsNeedingBackup const sessionsNeedingBackup = getJsonItem<{
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; [senderKeySessionId: string]: boolean;
}>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) { for (const session of sessions) {
sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true;
} }

View File

@ -283,21 +283,21 @@ export class QRCodeData {
private static generateBuffer(qrData: IQrData): Buffer { private static generateBuffer(qrData: IQrData): Buffer {
let buf = Buffer.alloc(0); // we'll concat our way through life let buf = Buffer.alloc(0); // we'll concat our way through life
const appendByte = (b): void => { const appendByte = (b: number): void => {
const tmpBuf = Buffer.from([b]); const tmpBuf = Buffer.from([b]);
buf = Buffer.concat([buf, tmpBuf]); buf = Buffer.concat([buf, tmpBuf]);
}; };
const appendInt = (i): void => { const appendInt = (i: number): void => {
const tmpBuf = Buffer.alloc(2); const tmpBuf = Buffer.alloc(2);
tmpBuf.writeInt16BE(i, 0); tmpBuf.writeInt16BE(i, 0);
buf = Buffer.concat([buf, tmpBuf]); buf = Buffer.concat([buf, tmpBuf]);
}; };
const appendStr = (s, enc, withLengthPrefix = true): void => { const appendStr = (s: string, enc: BufferEncoding, withLengthPrefix = true): void => {
const tmpBuf = Buffer.from(s, enc); const tmpBuf = Buffer.from(s, enc);
if (withLengthPrefix) appendInt(tmpBuf.byteLength); if (withLengthPrefix) appendInt(tmpBuf.byteLength);
buf = Buffer.concat([buf, tmpBuf]); buf = Buffer.concat([buf, tmpBuf]);
}; };
const appendEncBase64 = (b64): void => { const appendEncBase64 = (b64: string): void => {
const b = decodeBase64(b64); const b = decodeBase64(b64);
const tmpBuf = Buffer.from(b); const tmpBuf = Buffer.from(b);
buf = Buffer.concat([buf, tmpBuf]); buf = Buffer.concat([buf, tmpBuf]);
@ -307,7 +307,7 @@ export class QRCodeData {
appendStr(qrData.prefix, "ascii", false); appendStr(qrData.prefix, "ascii", false);
appendByte(qrData.version); appendByte(qrData.version);
appendByte(qrData.mode); appendByte(qrData.mode);
appendStr(qrData.transactionId, "utf-8"); appendStr(qrData.transactionId!, "utf-8");
appendEncBase64(qrData.firstKeyB64); appendEncBase64(qrData.firstKeyB64);
appendEncBase64(qrData.secondKeyB64); appendEncBase64(qrData.secondKeyB64);
appendEncBase64(qrData.secretB64); appendEncBase64(qrData.secretB64);

View File

@ -140,7 +140,7 @@ function generateEmojiSas(sasBytes: number[]): EmojiMapping[] {
const sasGenerators = { const sasGenerators = {
decimal: generateDecimalSas, decimal: generateDecimalSas,
emoji: generateEmojiSas, emoji: generateEmojiSas,
}; } as const;
export interface IGeneratedSas { export interface IGeneratedSas {
decimal?: [number, number, number]; decimal?: [number, number, number];
@ -154,11 +154,12 @@ export interface ISasEvent {
mismatch(): void; mismatch(): void;
} }
function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas { function generateSas(sasBytes: Uint8Array, methods: string[]): IGeneratedSas {
const sas: IGeneratedSas = {}; const sas: IGeneratedSas = {};
for (const method of methods) { for (const method of methods) {
if (method in sasGenerators) { if (method in sasGenerators) {
sas[method] = sasGenerators[method](sasBytes); // @ts-ignore - ts doesn't like us mixing types like this
sas[method] = sasGenerators[method](Array.from(sasBytes));
} }
} }
return sas; return sas;
@ -168,15 +169,14 @@ const macMethods = {
"hkdf-hmac-sha256": "calculate_mac", "hkdf-hmac-sha256": "calculate_mac",
"org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64", "org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64",
"hmac-sha256": "calculate_mac_long_kdf", "hmac-sha256": "calculate_mac_long_kdf",
}; } as const;
type Method = keyof typeof macMethods; type MacMethod = keyof typeof macMethods;
function calculateMAC(olmSAS: OlmSAS, method: Method) { function calculateMAC(olmSAS: OlmSAS, method: MacMethod) {
return function(...args): string { return function(input: string, info: string): string {
const macFunction = olmSAS[macMethods[method]]; const mac = olmSAS[macMethods[method]](input, info);
const mac: string = macFunction.apply(olmSAS, args); logger.log("SAS calculateMAC:", method, [input, info], mac);
logger.log("SAS calculateMAC:", method, args, mac);
return mac; return mac;
}; };
} }
@ -202,15 +202,17 @@ const calculateKeyAgreement = {
+ sas.channel.transactionId; + sas.channel.transactionId;
return olmSAS.generate_bytes(sasInfo, bytes); return olmSAS.generate_bytes(sasInfo, bytes);
}, },
}; } as const;
type KeyAgreement = keyof typeof calculateKeyAgreement;
/* lists of algorithms/methods that are supported. The key agreement, hashes, /* lists of algorithms/methods that are supported. The key agreement, hashes,
* and MAC lists should be sorted in order of preference (most preferred * and MAC lists should be sorted in order of preference (most preferred
* first). * first).
*/ */
const KEY_AGREEMENT_LIST = ["curve25519-hkdf-sha256", "curve25519"]; const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"];
const HASHES_LIST = ["sha256"]; const HASHES_LIST = ["sha256"];
const MAC_LIST: Method[] = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"]; const MAC_LIST: MacMethod[] = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"];
const SAS_LIST = Object.keys(sasGenerators); const SAS_LIST = Object.keys(sasGenerators);
const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST); const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST);
@ -299,10 +301,10 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
} }
private async verifyAndCheckMAC( private async verifyAndCheckMAC(
keyAgreement: string, keyAgreement: KeyAgreement,
sasMethods: string[], sasMethods: string[],
olmSAS: OlmSAS, olmSAS: OlmSAS,
macMethod: Method, macMethod: MacMethod,
): Promise<void> { ): Promise<void> {
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6); const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise<void>((resolve, reject) => { const verifySAS = new Promise<void>((resolve, reject) => {
@ -354,7 +356,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
throw new SwitchStartEventError(this.startEvent); throw new SwitchStartEventError(this.startEvent);
} }
let e; let e: MatrixEvent;
try { try {
e = await this.waitForEvent(EventType.KeyVerificationAccept); e = await this.waitForEvent(EventType.KeyVerificationAccept);
} finally { } finally {
@ -445,8 +447,8 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
} }
} }
private sendMAC(olmSAS: OlmSAS, method: Method): Promise<void> { private sendMAC(olmSAS: OlmSAS, method: MacMethod): Promise<void> {
const mac = {}; const mac: Record<string, string> = {};
const keyList: string[] = []; const keyList: string[] = [];
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.baseApis.getUserId() + this.baseApis.deviceId + this.baseApis.getUserId() + this.baseApis.deviceId
@ -455,7 +457,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
const deviceKeyId = `ed25519:${this.baseApis.deviceId}`; const deviceKeyId = `ed25519:${this.baseApis.deviceId}`;
mac[deviceKeyId] = calculateMAC(olmSAS, method)( mac[deviceKeyId] = calculateMAC(olmSAS, method)(
this.baseApis.getDeviceEd25519Key(), this.baseApis.getDeviceEd25519Key()!,
baseInfo + deviceKeyId, baseInfo + deviceKeyId,
); );
keyList.push(deviceKeyId); keyList.push(deviceKeyId);
@ -477,7 +479,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
return this.send(EventType.KeyVerificationMac, { mac, keys }); return this.send(EventType.KeyVerificationMac, { mac, keys });
} }
private async checkMAC(olmSAS: OlmSAS, content: IContent, method: Method): Promise<void> { private async checkMAC(olmSAS: OlmSAS, content: IContent, method: MacMethod): Promise<void> {
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.userId + this.deviceId + this.userId + this.deviceId
+ this.baseApis.getUserId() + this.baseApis.deviceId + this.baseApis.getUserId() + this.baseApis.deviceId

View File

@ -148,17 +148,17 @@ export class FilterComponent {
"types": function(v: string): boolean { "types": function(v: string): boolean {
return matchesWildcard(eventType, v); return matchesWildcard(eventType, v);
}, },
}; } as const;
for (const name in literalKeys) { for (const name in literalKeys) {
const matchFunc = literalKeys[name]; const matchFunc = literalKeys[<keyof typeof literalKeys>name];
const notName = "not_" + name; const notName = "not_" + name;
const disallowedValues: string[] = this.filterJson[notName]; const disallowedValues = this.filterJson[<`not_${keyof typeof literalKeys}`>notName];
if (disallowedValues?.some(matchFunc)) { if (disallowedValues?.some(matchFunc)) {
return false; return false;
} }
const allowedValues: string[] = this.filterJson[name]; const allowedValues = this.filterJson[name as keyof typeof literalKeys];
if (allowedValues && !allowedValues.some(matchFunc)) { if (allowedValues && !allowedValues.some(matchFunc)) {
return false; return false;
} }

View File

@ -31,8 +31,8 @@ import { MatrixEvent } from "./models/event";
* @param {string} keyNesting * @param {string} keyNesting
* @param {*} val * @param {*} val
*/ */
function setProp(obj: object, keyNesting: string, val: any): void { function setProp(obj: Record<string, any>, keyNesting: string, val: any): void {
const nestedKeys = keyNesting.split("."); const nestedKeys = keyNesting.split(".") as [keyof typeof obj];
let currentObj = obj; let currentObj = obj;
for (let i = 0; i < (nestedKeys.length - 1); i++) { for (let i = 0; i < (nestedKeys.length - 1); i++) {
if (!currentObj[nestedKeys[i]]) { if (!currentObj[nestedKeys[i]]) {
@ -70,8 +70,7 @@ interface IStateFilter extends IRoomEventFilter {}
interface IRoomFilter { interface IRoomFilter {
not_rooms?: string[]; not_rooms?: string[];
rooms?: string[]; rooms?: string[];ephemeral?: IRoomEventFilter;
ephemeral?: IRoomEventFilter;
include_leave?: boolean; include_leave?: boolean;
state?: IStateFilter; state?: IStateFilter;
timeline?: IRoomEventFilter; timeline?: IRoomEventFilter;

View File

@ -318,19 +318,19 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
// 'membership' at the event level (rather than the content level) is a legacy // 'membership' at the event level (rather than the content level) is a legacy
// field that Element never otherwise looks at, but it will still take up a lot // field that Element never otherwise looks at, but it will still take up a lot
// of space if we don't intern it. // of space if we don't intern it.
["state_key", "type", "sender", "room_id", "membership"].forEach((prop) => { (["state_key", "type", "sender", "room_id", "membership"] as const).forEach((prop) => {
if (typeof event[prop] !== "string") return; if (typeof event[prop] !== "string") return;
event[prop] = internaliseString(event[prop]); event[prop] = internaliseString(event[prop]!);
}); });
["membership", "avatar_url", "displayname"].forEach((prop) => { (["membership", "avatar_url", "displayname"] as const).forEach((prop) => {
if (typeof event.content?.[prop] !== "string") return; if (typeof event.content?.[prop] !== "string") return;
event.content[prop] = internaliseString(event.content[prop]); event.content[prop] = internaliseString(event.content[prop]!);
}); });
["rel_type"].forEach((prop) => { (["rel_type"] as const).forEach((prop) => {
if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return; if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return;
event.content["m.relates_to"][prop] = internaliseString(event.content["m.relates_to"][prop]); event.content["m.relates_to"][prop] = internaliseString(event.content["m.relates_to"][prop]!);
}); });
this.txnId = event.txn_id; this.txnId = event.txn_id;
@ -1120,7 +1120,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
for (const key in this.event) { for (const key in this.event) {
if (this.event.hasOwnProperty(key) && !REDACT_KEEP_KEYS.has(key)) { if (this.event.hasOwnProperty(key) && !REDACT_KEEP_KEYS.has(key)) {
delete this.event[key]; delete this.event[key as keyof IEvent];
} }
} }
@ -1129,7 +1129,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
this.clearEvent = undefined; this.clearEvent = undefined;
} }
const keeps = REDACT_KEEP_CONTENT_MAP[this.getType()] || {}; const keeps = this.getType() in REDACT_KEEP_CONTENT_MAP
? REDACT_KEEP_CONTENT_MAP[this.getType() as keyof typeof REDACT_KEEP_CONTENT_MAP]
: {};
const content = this.getContent(); const content = this.getContent();
for (const key in content) { for (const key in content) {
if (content.hasOwnProperty(key) && !keeps[key]) { if (content.hasOwnProperty(key) && !keeps[key]) {
@ -1212,7 +1214,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* *
* @returns {object} The redaction event JSON, or an empty object * @returns {object} The redaction event JSON, or an empty object
*/ */
public getRedactionEvent(): object | null { public getRedactionEvent(): IEvent | {} | null {
if (!this.isRedacted()) return null; if (!this.isRedacted()) return null;
if (this.clearEvent?.unsigned) { if (this.clearEvent?.unsigned) {
@ -1512,7 +1514,8 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
const ev = new MatrixEvent(JSON.parse(JSON.stringify(this.event))); const ev = new MatrixEvent(JSON.parse(JSON.stringify(this.event)));
for (const [p, v] of Object.entries(this)) { for (const [p, v] of Object.entries(this)) {
if (p !== "event") { // exclude the thing we just cloned if (p !== "event") { // exclude the thing we just cloned
ev[p] = v; // @ts-ignore - XXX: this is just nasty
ev[p as keyof MatrixEvent] = v;
} }
} }
return ev; return ev;
@ -1612,7 +1615,7 @@ const REDACT_KEEP_KEYS = new Set([
]); ]);
// a map from state event type to the .content keys we keep when an event is redacted // a map from state event type to the .content keys we keep when an event is redacted
const REDACT_KEEP_CONTENT_MAP = { const REDACT_KEEP_CONTENT_MAP: Record<string, Record<string, 1>> = {
[EventType.RoomMember]: { 'membership': 1 }, [EventType.RoomMember]: { 'membership': 1 },
[EventType.RoomCreate]: { 'creator': 1 }, [EventType.RoomCreate]: { 'creator': 1 },
[EventType.RoomJoinRules]: { 'join_rule': 1 }, [EventType.RoomJoinRules]: { 'join_rule': 1 },
@ -1621,7 +1624,7 @@ const REDACT_KEEP_CONTENT_MAP = {
'kick': 1, 'redact': 1, 'state_default': 1, 'kick': 1, 'redact': 1, 'state_default': 1,
'users': 1, 'users_default': 1, 'users': 1, 'users_default': 1,
}, },
}; } as const;
/** /**
* Fires when an event is decrypted * Fires when an event is decrypted

View File

@ -17,7 +17,7 @@ limitations under the License.
import { UnstableValue } from "matrix-events-sdk"; import { UnstableValue } from "matrix-events-sdk";
import { MatrixClient } from "../client"; import { MatrixClient } from "../client";
import { MatrixEvent } from "./event"; import { IContent, MatrixEvent } from "./event";
import { EventTimeline } from "./event-timeline"; import { EventTimeline } from "./event-timeline";
import { Preset } from "../@types/partials"; import { Preset } from "../@types/partials";
import { globToRegexp } from "../utils"; import { globToRegexp } from "../utils";
@ -262,7 +262,7 @@ export class IgnoredInvites {
*/ */
public async getOrCreateSourceRooms(): Promise<Room[]> { public async getOrCreateSourceRooms(): Promise<Room[]> {
const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies(); const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies();
let sources = ignoreInvitesPolicies.sources; let sources: string[] = ignoreInvitesPolicies.sources;
// Validate `sources`. If it is invalid, trash out the current `sources` // Validate `sources`. If it is invalid, trash out the current `sources`
// and create a new list of sources from `target`. // and create a new list of sources from `target`.
@ -272,11 +272,11 @@ export class IgnoredInvites {
hasChanges = true; hasChanges = true;
sources = []; sources = [];
} }
let sourceRooms: Room[] = sources let sourceRooms = sources
// `sources` could contain non-string / invalid room ids // `sources` could contain non-string / invalid room ids
.filter(roomId => typeof roomId === "string") .filter(roomId => typeof roomId === "string")
.map(roomId => this.client.getRoom(roomId)) .map(roomId => this.client.getRoom(roomId))
.filter(room => !!room); .filter(room => !!room) as Room[];
if (sourceRooms.length != sources.length) { if (sourceRooms.length != sources.length) {
hasChanges = true; hasChanges = true;
} }
@ -327,7 +327,7 @@ export class IgnoredInvites {
*/ */
private getPoliciesAndIgnoreInvitesPolicies(): private getPoliciesAndIgnoreInvitesPolicies():
{policies: {[key: string]: any}, ignoreInvitesPolicies: {[key: string]: any}} { {policies: {[key: string]: any}, ignoreInvitesPolicies: {[key: string]: any}} {
let policies = {}; let policies: IContent = {};
for (const key of [POLICIES_ACCOUNT_EVENT_TYPE.name, POLICIES_ACCOUNT_EVENT_TYPE.altName]) { for (const key of [POLICIES_ACCOUNT_EVENT_TYPE.name, POLICIES_ACCOUNT_EVENT_TYPE.altName]) {
if (!key) { if (!key) {
continue; continue;

View File

@ -22,7 +22,7 @@ import { RoomMember } from "./room-member";
import { logger } from '../logger'; import { logger } from '../logger';
import * as utils from "../utils"; import * as utils from "../utils";
import { EventType, UNSTABLE_MSC2716_MARKER } from "../@types/event"; import { EventType, UNSTABLE_MSC2716_MARKER } from "../@types/event";
import { MatrixEvent, MatrixEventEvent } from "./event"; import { IEvent, MatrixEvent, MatrixEventEvent } from "./event";
import { MatrixClient } from "../client"; import { MatrixClient } from "../client";
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials"; import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
import { TypedEventEmitter } from "./typed-event-emitter"; import { TypedEventEmitter } from "./typed-event-emitter";
@ -53,6 +53,20 @@ enum OobStatus {
Finished, Finished,
} }
export interface IPowerLevelsContent {
users?: Record<string, number>;
events?: Record<string, number>;
// eslint-disable-next-line camelcase
users_default?: number;
// eslint-disable-next-line camelcase
events_default?: number;
// eslint-disable-next-line camelcase
state_default?: number;
ban?: number;
kick?: number;
redact?: number;
}
export enum RoomStateEvent { export enum RoomStateEvent {
Events = "RoomState.events", Events = "RoomState.events",
Members = "RoomState.members", Members = "RoomState.members",
@ -499,7 +513,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
const beacon = this.beacons.get(beaconIdentifier)!; const beacon = this.beacons.get(beaconIdentifier)!;
if (event.isRedacted()) { if (event.isRedacted()) {
if (beacon.beaconInfoId === event.getRedactionEvent()?.['redacts']) { if (beacon.beaconInfoId === (<IEvent>event.getRedactionEvent())?.redacts) {
beacon.destroy(); beacon.destroy();
this.beacons.delete(beaconIdentifier); this.beacons.delete(beaconIdentifier);
} }
@ -724,17 +738,17 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
* @param {number} powerLevel The power level of the member * @param {number} powerLevel The power level of the member
* @return {boolean} true if the given power level is sufficient * @return {boolean} true if the given power level is sufficient
*/ */
public hasSufficientPowerLevelFor(action: string, powerLevel: number): boolean { public hasSufficientPowerLevelFor(action: "ban" | "kick" | "redact", powerLevel: number): boolean {
const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, ""); const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
let powerLevels = {}; let powerLevels: IPowerLevelsContent = {};
if (powerLevelsEvent) { if (powerLevelsEvent) {
powerLevels = powerLevelsEvent.getContent(); powerLevels = powerLevelsEvent.getContent();
} }
let requiredLevel = 50; let requiredLevel = 50;
if (utils.isNumber(powerLevels[action])) { if (utils.isNumber(powerLevels[action])) {
requiredLevel = powerLevels[action]; requiredLevel = powerLevels[action]!;
} }
return powerLevel >= requiredLevel; return powerLevel >= requiredLevel;
@ -807,8 +821,8 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
private maySendEventOfType(eventType: EventType | string, userId: string, state: boolean): boolean { private maySendEventOfType(eventType: EventType | string, userId: string, state: boolean): boolean {
const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, ''); const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, '');
let powerLevels; let powerLevels: IPowerLevelsContent;
let eventsLevels = {}; let eventsLevels: Record<EventType | string, number> = {};
let stateDefault = 0; let stateDefault = 0;
let eventsDefault = 0; let eventsDefault = 0;
@ -818,20 +832,20 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
eventsLevels = powerLevels.events || {}; eventsLevels = powerLevels.events || {};
if (Number.isSafeInteger(powerLevels.state_default)) { if (Number.isSafeInteger(powerLevels.state_default)) {
stateDefault = powerLevels.state_default; stateDefault = powerLevels.state_default!;
} else { } else {
stateDefault = 50; stateDefault = 50;
} }
const userPowerLevel = powerLevels.users && powerLevels.users[userId]; const userPowerLevel = powerLevels.users && powerLevels.users[userId];
if (Number.isSafeInteger(userPowerLevel)) { if (Number.isSafeInteger(userPowerLevel)) {
powerLevel = userPowerLevel; powerLevel = userPowerLevel!;
} else if (Number.isSafeInteger(powerLevels.users_default)) { } else if (Number.isSafeInteger(powerLevels.users_default)) {
powerLevel = powerLevels.users_default; powerLevel = powerLevels.users_default!;
} }
if (Number.isSafeInteger(powerLevels.events_default)) { if (Number.isSafeInteger(powerLevels.events_default)) {
eventsDefault = powerLevels.events_default; eventsDefault = powerLevels.events_default!;
} }
} }
@ -877,7 +891,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
*/ */
public getJoinRule(): JoinRule { public getJoinRule(): JoinRule {
const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, ""); const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
const joinRuleContent = joinRuleEvent?.getContent<IJoinRuleEventContent>() ?? {}; const joinRuleContent: Partial<IJoinRuleEventContent> = joinRuleEvent?.getContent() ?? {};
return joinRuleContent["join_rule"] || JoinRule.Invite; return joinRuleContent["join_rule"] || JoinRule.Invite;
} }

View File

@ -20,8 +20,8 @@ limitations under the License.
export interface IRoomSummary { export interface IRoomSummary {
"m.heroes": string[]; "m.heroes": string[];
"m.joined_member_count": number; "m.joined_member_count"?: number;
"m.invited_member_count": number; "m.invited_member_count"?: number;
} }
interface IInfo { interface IInfo {

View File

@ -1312,10 +1312,10 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
const joinedCount = summary["m.joined_member_count"]; const joinedCount = summary["m.joined_member_count"];
const invitedCount = summary["m.invited_member_count"]; const invitedCount = summary["m.invited_member_count"];
if (Number.isInteger(joinedCount)) { if (Number.isInteger(joinedCount)) {
this.currentState.setJoinedMemberCount(joinedCount); this.currentState.setJoinedMemberCount(joinedCount!);
} }
if (Number.isInteger(invitedCount)) { if (Number.isInteger(invitedCount)) {
this.currentState.setInvitedMemberCount(invitedCount); this.currentState.setInvitedMemberCount(invitedCount!);
} }
if (Array.isArray(heroes)) { if (Array.isArray(heroes)) {
// be cautious about trusting server values, // be cautious about trusting server values,
@ -1826,7 +1826,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
timelineSet.getFilter(), timelineSet.getFilter(),
); );
timelineSet.getLiveTimeline().setPaginationToken(end, Direction.Backward); timelineSet.getLiveTimeline().setPaginationToken(end ?? null, Direction.Backward);
if (!events.length) return; if (!events.length) return;

View File

@ -518,13 +518,13 @@ export class PushProcessor {
* @return {object} The push rule, or null if no such rule was found * @return {object} The push rule, or null if no such rule was found
*/ */
public getPushRuleById(ruleId: string): IPushRule | null { public getPushRuleById(ruleId: string): IPushRule | null {
for (const scope of ['global']) { for (const scope of ['global'] as const) {
if (this.client.pushRules?.[scope] === undefined) continue; if (this.client.pushRules?.[scope] === undefined) continue;
for (const kind of RULEKINDS_IN_ORDER) { for (const kind of RULEKINDS_IN_ORDER) {
if (this.client.pushRules[scope][kind] === undefined) continue; if (this.client.pushRules[scope][kind] === undefined) continue;
for (const rule of this.client.pushRules[scope][kind]) { for (const rule of this.client.pushRules[scope][kind]!) {
if (rule.rule_id === ruleId) return rule; if (rule.rule_id === ruleId) return rule;
} }
} }

View File

@ -97,7 +97,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
* @see module:scheduler~queueAlgorithm * @see module:scheduler~queueAlgorithm
*/ */
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
public static QUEUE_MESSAGES(event: MatrixEvent): "message" | null { public static QUEUE_MESSAGES(event: MatrixEvent): string | null {
// enqueue messages or events that associate with another event (redactions and relations) // enqueue messages or events that associate with another event (redactions and relations)
if (event.getType() === EventType.RoomMessage || event.hasAssociation()) { if (event.getType() === EventType.RoomMessage || event.hasAssociation()) {
// put these events in the 'message' queue. // put these events in the 'message' queue.

View File

@ -22,7 +22,7 @@ import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } fr
import { ISyncStateData, SyncState, _createAndReEmitRoom } from "./sync"; import { ISyncStateData, SyncState, _createAndReEmitRoom } from "./sync";
import { MatrixEvent } from "./models/event"; import { MatrixEvent } from "./models/event";
import { Crypto } from "./crypto"; import { Crypto } from "./crypto";
import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState } from "./sync-accumulator"; import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState, ISyncResponse } from "./sync-accumulator";
import { MatrixError } from "./http-api"; import { MatrixError } from "./http-api";
import { import {
Extension, Extension,
@ -44,7 +44,18 @@ import { RoomMemberEvent } from "./models/room-member";
// keepAlive is successful but the server /sync fails. // keepAlive is successful but the server /sync fails.
const FAILED_SYNC_ERROR_THRESHOLD = 3; const FAILED_SYNC_ERROR_THRESHOLD = 3;
class ExtensionE2EE implements Extension { type ExtensionE2EERequest = {
enabled: boolean;
};
type ExtensionE2EEResponse = Pick<ISyncResponse,
"device_lists" |
"device_one_time_keys_count" |
"device_unused_fallback_key_types" |
"org.matrix.msc2732.device_unused_fallback_key_types"
>;
class ExtensionE2EE implements Extension<ExtensionE2EERequest, ExtensionE2EEResponse> {
public constructor(private readonly crypto: Crypto) {} public constructor(private readonly crypto: Crypto) {}
public name(): string { public name(): string {
@ -55,7 +66,7 @@ class ExtensionE2EE implements Extension {
return ExtensionState.PreProcess; return ExtensionState.PreProcess;
} }
public onRequest(isInitial: boolean): object | undefined { public onRequest(isInitial: boolean): ExtensionE2EERequest | undefined {
if (!isInitial) { if (!isInitial) {
return undefined; return undefined;
} }
@ -64,7 +75,7 @@ class ExtensionE2EE implements Extension {
}; };
} }
public async onResponse(data: object): Promise<void> { public async onResponse(data: ExtensionE2EEResponse): Promise<void> {
// Handle device list updates // Handle device list updates
if (data["device_lists"]) { if (data["device_lists"]) {
await this.crypto.handleDeviceListChanges({ await this.crypto.handleDeviceListChanges({
@ -92,7 +103,18 @@ class ExtensionE2EE implements Extension {
} }
} }
class ExtensionToDevice implements Extension { type ExtensionToDeviceRequest = {
since?: string;
limit?: number;
enabled?: boolean;
};
type ExtensionToDeviceResponse = {
events: Required<ISyncResponse>["to_device"]["events"];
next_batch: string | null;
};
class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, ExtensionToDeviceResponse> {
private nextBatch: string | null = null; private nextBatch: string | null = null;
public constructor(private readonly client: MatrixClient) {} public constructor(private readonly client: MatrixClient) {}
@ -105,8 +127,8 @@ class ExtensionToDevice implements Extension {
return ExtensionState.PreProcess; return ExtensionState.PreProcess;
} }
public onRequest(isInitial: boolean): object { public onRequest(isInitial: boolean): ExtensionToDeviceRequest {
const extReq = { const extReq: ExtensionToDeviceRequest = {
since: this.nextBatch !== null ? this.nextBatch : undefined, since: this.nextBatch !== null ? this.nextBatch : undefined,
}; };
if (isInitial) { if (isInitial) {
@ -116,11 +138,10 @@ class ExtensionToDevice implements Extension {
return extReq; return extReq;
} }
public async onResponse(data: object): Promise<void> { public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
const cancelledKeyVerificationTxns: string[] = []; const cancelledKeyVerificationTxns: string[] = [];
data["events"] = data["events"] || []; data.events
data["events"] ?.map(this.client.getEventMapper())
.map(this.client.getEventMapper())
.map((toDeviceEvent) => { // map is a cheap inline forEach .map((toDeviceEvent) => { // map is a cheap inline forEach
// We want to flag m.key.verification.start events as cancelled // We want to flag m.key.verification.start events as cancelled
// if there's an accompanying m.key.verification.cancel event, so // if there's an accompanying m.key.verification.cancel event, so
@ -165,11 +186,20 @@ class ExtensionToDevice implements Extension {
}, },
); );
this.nextBatch = data["next_batch"]; this.nextBatch = data.next_batch;
} }
} }
class ExtensionAccountData implements Extension { type ExtensionAccountDataRequest = {
enabled: boolean;
};
type ExtensionAccountDataResponse = {
global: IMinimalEvent[];
rooms: Record<string, IMinimalEvent[]>;
};
class ExtensionAccountData implements Extension<ExtensionAccountDataRequest, ExtensionAccountDataResponse> {
public constructor(private readonly client: MatrixClient) {} public constructor(private readonly client: MatrixClient) {}
public name(): string { public name(): string {
@ -180,7 +210,7 @@ class ExtensionAccountData implements Extension {
return ExtensionState.PostProcess; return ExtensionState.PostProcess;
} }
public onRequest(isInitial: boolean): object | undefined { public onRequest(isInitial: boolean): ExtensionAccountDataRequest | undefined {
if (!isInitial) { if (!isInitial) {
return undefined; return undefined;
} }
@ -189,7 +219,7 @@ class ExtensionAccountData implements Extension {
}; };
} }
public onResponse(data: {global: object[], rooms: Record<string, object[]>}): void { public onResponse(data: ExtensionAccountDataResponse): void {
if (data.global && data.global.length > 0) { if (data.global && data.global.length > 0) {
this.processGlobalAccountData(data.global); this.processGlobalAccountData(data.global);
} }
@ -208,9 +238,9 @@ class ExtensionAccountData implements Extension {
} }
} }
private processGlobalAccountData(globalAccountData: object[]): void { private processGlobalAccountData(globalAccountData: IMinimalEvent[]): void {
const events = mapEvents(this.client, undefined, globalAccountData); const events = mapEvents(this.client, undefined, globalAccountData);
const prevEventsMap = events.reduce((m, c) => { const prevEventsMap = events.reduce<Record<string, MatrixEvent | undefined>>((m, c) => {
m[c.getType()] = this.client.store.getAccountData(c.getType()); m[c.getType()] = this.client.store.getAccountData(c.getType());
return m; return m;
}, {}); }, {});
@ -233,7 +263,15 @@ class ExtensionAccountData implements Extension {
} }
} }
class ExtensionTyping implements Extension { type ExtensionTypingRequest = {
enabled: boolean;
};
type ExtensionTypingResponse = {
rooms: Record<string, IMinimalEvent>;
};
class ExtensionTyping implements Extension<ExtensionTypingRequest, ExtensionTypingResponse> {
public constructor(private readonly client: MatrixClient) {} public constructor(private readonly client: MatrixClient) {}
public name(): string { public name(): string {
@ -244,7 +282,7 @@ class ExtensionTyping implements Extension {
return ExtensionState.PostProcess; return ExtensionState.PostProcess;
} }
public onRequest(isInitial: boolean): object | undefined { public onRequest(isInitial: boolean): ExtensionTypingRequest | undefined {
if (!isInitial) { if (!isInitial) {
return undefined; // don't send a JSON object for subsequent requests, we don't need to. return undefined; // don't send a JSON object for subsequent requests, we don't need to.
} }
@ -253,20 +291,26 @@ class ExtensionTyping implements Extension {
}; };
} }
public onResponse(data: {rooms: Record<string, IMinimalEvent>}): void { public onResponse(data: ExtensionTypingResponse): void {
if (!data || !data.rooms) { if (!data?.rooms) {
return; return;
} }
for (const roomId in data.rooms) { for (const roomId in data.rooms) {
processEphemeralEvents( processEphemeralEvents(this.client, roomId, [data.rooms[roomId]]);
this.client, roomId, [data.rooms[roomId]],
);
} }
} }
} }
class ExtensionReceipts implements Extension { type ExtensionReceiptsRequest = {
enabled: boolean;
};
type ExtensionReceiptsResponse = {
rooms: Record<string, IMinimalEvent>;
};
class ExtensionReceipts implements Extension<ExtensionReceiptsRequest, ExtensionReceiptsResponse> {
public constructor(private readonly client: MatrixClient) {} public constructor(private readonly client: MatrixClient) {}
public name(): string { public name(): string {
@ -277,7 +321,7 @@ class ExtensionReceipts implements Extension {
return ExtensionState.PostProcess; return ExtensionState.PostProcess;
} }
public onRequest(isInitial: boolean): object | undefined { public onRequest(isInitial: boolean): ExtensionReceiptsRequest | undefined {
if (isInitial) { if (isInitial) {
return { return {
enabled: true, enabled: true,
@ -286,8 +330,8 @@ class ExtensionReceipts implements Extension {
return undefined; // don't send a JSON object for subsequent requests, we don't need to. return undefined; // don't send a JSON object for subsequent requests, we don't need to.
} }
public onResponse(data: {rooms: Record<string, IMinimalEvent>}): void { public onResponse(data: ExtensionReceiptsResponse): void {
if (!data || !data.rooms) { if (!data?.rooms) {
return; return;
} }
@ -336,7 +380,7 @@ export class SlidingSyncSdk {
this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this)); this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this));
this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this)); this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this));
const extensions: Extension[] = [ const extensions: Extension<any, any>[] = [
new ExtensionToDevice(this.client), new ExtensionToDevice(this.client),
new ExtensionAccountData(this.client), new ExtensionAccountData(this.client),
new ExtensionTyping(this.client), new ExtensionTyping(this.client),
@ -533,7 +577,7 @@ export class SlidingSyncSdk {
// room::decryptCriticalEvent is in charge of decrypting all the events // room::decryptCriticalEvent is in charge of decrypting all the events
// required for a client to function properly // required for a client to function properly
let timelineEvents = mapEvents(this.client, room.roomId, roomData.timeline, false); let timelineEvents = mapEvents(this.client, room.roomId, roomData.timeline, false);
const ephemeralEvents = []; // TODO this.mapSyncEventsFormat(joinObj.ephemeral); const ephemeralEvents: MatrixEvent[] = []; // TODO this.mapSyncEventsFormat(joinObj.ephemeral);
// TODO: handle threaded / beacon events // TODO: handle threaded / beacon events
@ -967,12 +1011,14 @@ function ensureNameEvent(client: MatrixClient, roomId: string, roomData: MSC3575
return roomData; return roomData;
} }
type TaggedEvent = (IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent) & { room_id?: string };
// Helper functions which set up JS SDK structs are below and are identical to the sync v2 counterparts, // Helper functions which set up JS SDK structs are below and are identical to the sync v2 counterparts,
// just outside the class. // just outside the class.
function mapEvents(client: MatrixClient, roomId: string | undefined, events: object[], decrypt = true): MatrixEvent[] { function mapEvents(client: MatrixClient, roomId: string | undefined, events: object[], decrypt = true): MatrixEvent[] {
const mapper = client.getEventMapper({ decrypt }); const mapper = client.getEventMapper({ decrypt });
return (events as Array<IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent>).map(function(e) { return (events as TaggedEvent[]).map(function(e) {
e["room_id"] = roomId; e.room_id = roomId;
return mapper(e); return mapper(e);
}); });
} }

View File

@ -139,7 +139,7 @@ export interface MSC3575SlidingSyncResponse {
txn_id?: string; txn_id?: string;
lists: ListResponse[]; lists: ListResponse[];
rooms: Record<string, MSC3575RoomData>; rooms: Record<string, MSC3575RoomData>;
extensions: object; extensions: Record<string, object>;
} }
export enum SlidingSyncState { export enum SlidingSyncState {
@ -265,7 +265,7 @@ export enum ExtensionState {
/** /**
* An interface that must be satisfied to register extensions * An interface that must be satisfied to register extensions
*/ */
export interface Extension { export interface Extension<Req extends {}, Res extends {}> {
/** /**
* The extension name to go under 'extensions' in the request body. * The extension name to go under 'extensions' in the request body.
* @returns The JSON key. * @returns The JSON key.
@ -277,12 +277,12 @@ export interface Extension {
* @param isInitial True when this is part of the initial request (send sticky params) * @param isInitial True when this is part of the initial request (send sticky params)
* @returns The request JSON to send. * @returns The request JSON to send.
*/ */
onRequest(isInitial: boolean): object | undefined; onRequest(isInitial: boolean): Req | undefined;
/** /**
* A function which is called when there is response JSON under this extension. * A function which is called when there is response JSON under this extension.
* @param data The response JSON under the extension name. * @param data The response JSON under the extension name.
*/ */
onResponse(data: object); onResponse(data: Res): void;
/** /**
* Controls when onResponse should be called. * Controls when onResponse should be called.
* @returns The state when it should be called. * @returns The state when it should be called.
@ -353,7 +353,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
// a defer to resolve/reject depending on whether they were successfully sent or not. // a defer to resolve/reject depending on whether they were successfully sent or not.
private txnIdDefers: (IDeferred<string> & { txnId: string})[] = []; private txnIdDefers: (IDeferred<string> & { txnId: string})[] = [];
// map of extension name to req/resp handler // map of extension name to req/resp handler
private extensions: Record<string, Extension> = {}; private extensions: Record<string, Extension<any, any>> = {};
private desiredRoomSubscriptions = new Set<string>(); // the *desired* room subscriptions private desiredRoomSubscriptions = new Set<string>(); // the *desired* room subscriptions
private confirmedRoomSubscriptions = new Set<string>(); private confirmedRoomSubscriptions = new Set<string>();
@ -519,22 +519,22 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* Register an extension to send with the /sync request. * Register an extension to send with the /sync request.
* @param ext The extension to register. * @param ext The extension to register.
*/ */
public registerExtension(ext: Extension): void { public registerExtension(ext: Extension<any, any>): void {
if (this.extensions[ext.name()]) { if (this.extensions[ext.name()]) {
throw new Error(`registerExtension: ${ext.name()} already exists as an extension`); throw new Error(`registerExtension: ${ext.name()} already exists as an extension`);
} }
this.extensions[ext.name()] = ext; this.extensions[ext.name()] = ext;
} }
private getExtensionRequest(isInitial: boolean): object { private getExtensionRequest(isInitial: boolean): Record<string, object | undefined> {
const ext = {}; const ext: Record<string, object | undefined> = {};
Object.keys(this.extensions).forEach((extName) => { Object.keys(this.extensions).forEach((extName) => {
ext[extName] = this.extensions[extName].onRequest(isInitial); ext[extName] = this.extensions[extName].onRequest(isInitial);
}); });
return ext; return ext;
} }
private onPreExtensionsResponse(ext: object): void { private onPreExtensionsResponse(ext: Record<string, object>): void {
Object.keys(ext).forEach((extName) => { Object.keys(ext).forEach((extName) => {
if (this.extensions[extName].when() == ExtensionState.PreProcess) { if (this.extensions[extName].when() == ExtensionState.PreProcess) {
this.extensions[extName].onResponse(ext[extName]); this.extensions[extName].onResponse(ext[extName]);
@ -542,7 +542,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
}); });
} }
private onPostExtensionsResponse(ext: object): void { private onPostExtensionsResponse(ext: Record<string, object>): void {
Object.keys(ext).forEach((extName) => { Object.keys(ext).forEach((extName) => {
if (this.extensions[extName].when() == ExtensionState.PostProcess) { if (this.extensions[extName].when() == ExtensionState.PostProcess) {
this.extensions[extName].onResponse(ext[extName]); this.extensions[extName].onResponse(ext[extName]);

View File

@ -123,7 +123,7 @@ export interface IStore {
* @param {string} token The token associated with these events. * @param {string} token The token associated with these events.
* @param {boolean} toStart True if these are paginated results. * @param {boolean} toStart True if these are paginated results.
*/ */
storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void; storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void;
/** /**
* Store a filter. * Store a filter.

View File

@ -315,10 +315,10 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
const minStateKeyProm = reqAsCursorPromise( const minStateKeyProm = reqAsCursorPromise(
roomIndex.openKeyCursor(roomRange, "next"), roomIndex.openKeyCursor(roomRange, "next"),
).then((cursor) => cursor && cursor.primaryKey[1]); ).then((cursor) => (<IDBValidKey[]>cursor?.primaryKey)[1]);
const maxStateKeyProm = reqAsCursorPromise( const maxStateKeyProm = reqAsCursorPromise(
roomIndex.openKeyCursor(roomRange, "prev"), roomIndex.openKeyCursor(roomRange, "prev"),
).then((cursor) => cursor && cursor.primaryKey[1]); ).then((cursor) => (<IDBValidKey[]>cursor?.primaryKey)[1]);
const [minStateKey, maxStateKey] = await Promise.all( const [minStateKey, maxStateKey] = await Promise.all(
[minStateKeyProm, maxStateKeyProm]); [minStateKeyProm, maxStateKeyProm]);

View File

@ -217,7 +217,7 @@ export class MemoryStore implements IStore {
* @param {string} token The token associated with these events. * @param {string} token The token associated with these events.
* @param {boolean} toStart True if these are paginated results. * @param {boolean} toStart True if these are paginated results.
*/ */
public storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void { public storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void {
// no-op because they've already been added to the room instance. // no-op because they've already been added to the room instance.
} }

View File

@ -139,7 +139,7 @@ export class StubStore implements IStore {
* @param {string} token The token associated with these events. * @param {string} token The token associated with these events.
* @param {boolean} toStart True if these are paginated results. * @param {boolean} toStart True if these are paginated results.
*/ */
public storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void {} public storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void {}
/** /**
* Store a filter. * Store a filter.

View File

@ -116,7 +116,7 @@ interface IAccountData {
events: IMinimalEvent[]; events: IMinimalEvent[];
} }
interface IToDeviceEvent { export interface IToDeviceEvent {
content: IContent; content: IContent;
sender: string; sender: string;
type: string; type: string;
@ -127,8 +127,8 @@ interface IToDevice {
} }
interface IDeviceLists { interface IDeviceLists {
changed: string[]; changed?: string[];
left: string[]; left?: string[];
} }
export interface ISyncResponse { export interface ISyncResponse {
@ -139,6 +139,9 @@ export interface ISyncResponse {
to_device?: IToDevice; to_device?: IToDevice;
device_lists?: IDeviceLists; device_lists?: IDeviceLists;
device_one_time_keys_count?: Record<string, number>; device_one_time_keys_count?: Record<string, number>;
device_unused_fallback_key_types?: string[];
"org.matrix.msc2732.device_unused_fallback_key_types"?: string[];
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
@ -182,6 +185,12 @@ export interface ISyncData {
roomsData: IRooms; roomsData: IRooms;
} }
type TaggedEvent = IRoomEvent & { _localTs?: number };
function isTaggedEvent(event: IRoomEvent): event is TaggedEvent {
return "_localTs" in event && event["_localTs"] !== undefined;
}
/** /**
* The purpose of this class is to accumulate /sync responses such that a * The purpose of this class is to accumulate /sync responses such that a
* complete "initial" JSON response can be returned which accurately represents * complete "initial" JSON response can be returned which accurately represents
@ -473,35 +482,31 @@ export class SyncAccumulator {
// - existing state which didn't come down /sync. // - existing state which didn't come down /sync.
// - State events under the 'state' key. // - State events under the 'state' key.
// - State events in the 'timeline'. // - State events in the 'timeline'.
if (data.state && data.state.events) { data.state?.events?.forEach((e) => {
data.state.events.forEach((e) => { setState(currentData._currentState, e);
setState(currentData._currentState, e); });
}); data.timeline?.events?.forEach((e, index) => {
} // this nops if 'e' isn't a state event
if (data.timeline && data.timeline.events) { setState(currentData._currentState, e);
data.timeline.events.forEach((e, index) => { // append the event to the timeline. The back-pagination token
// this nops if 'e' isn't a state event // corresponds to the first event in the timeline
setState(currentData._currentState, e); let transformedEvent: TaggedEvent;
// append the event to the timeline. The back-pagination token if (!fromDatabase) {
// corresponds to the first event in the timeline transformedEvent = Object.assign({}, e);
let transformedEvent: IRoomEvent & { _localTs?: number }; if (transformedEvent.unsigned !== undefined) {
if (!fromDatabase) { transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
transformedEvent = Object.assign({}, e);
if (transformedEvent.unsigned !== undefined) {
transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
}
const age = e.unsigned ? e.unsigned.age : e.age;
if (age !== undefined) transformedEvent._localTs = Date.now() - age;
} else {
transformedEvent = e;
} }
const age = e.unsigned ? e.unsigned.age : e.age;
if (age !== undefined) transformedEvent._localTs = Date.now() - age;
} else {
transformedEvent = e;
}
currentData._timeline.push({ currentData._timeline.push({
event: transformedEvent, event: transformedEvent,
token: index === 0 ? (data.timeline.prev_batch ?? null) : null, token: index === 0 ? (data.timeline.prev_batch ?? null) : null,
});
}); });
} });
// attempt to prune the timeline by jumping between events which have // attempt to prune the timeline by jumping between events which have
// pagination tokens. // pagination tokens.
@ -581,7 +586,7 @@ export class SyncAccumulator {
room_id: roomId, room_id: roomId,
content: { content: {
// $event_id: { "m.read": { $user_id: $json } } // $event_id: { "m.read": { $user_id: $json } }
}, } as IContent,
}; };
for (const [userId, receiptData] of Object.entries(roomData._readReceipts)) { for (const [userId, receiptData] of Object.entries(roomData._readReceipts)) {
@ -626,8 +631,8 @@ export class SyncAccumulator {
} }
let transformedEvent: (IRoomEvent | IStateEvent) & { _localTs?: number }; let transformedEvent: (IRoomEvent | IStateEvent) & { _localTs?: number };
if (!forDatabase && msgData.event["_localTs"]) { if (!forDatabase && isTaggedEvent(msgData.event)) {
// This means we have to copy each event so we can fix it up to // This means we have to copy each event, so we can fix it up to
// set a correct 'age' parameter whilst keeping the local timestamp // set a correct 'age' parameter whilst keeping the local timestamp
// on our stored event. If this turns out to be a bottleneck, it could // on our stored event. If this turns out to be a bottleneck, it could
// be optimised either by doing this in the main process after the data // be optimised either by doing this in the main process after the data
@ -641,7 +646,7 @@ export class SyncAccumulator {
} }
delete transformedEvent._localTs; delete transformedEvent._localTs;
transformedEvent.unsigned = transformedEvent.unsigned || {}; transformedEvent.unsigned = transformedEvent.unsigned || {};
transformedEvent.unsigned.age = Date.now() - msgData.event["_localTs"]; transformedEvent.unsigned.age = Date.now() - msgData.event._localTs!;
} else { } else {
transformedEvent = msgData.event; transformedEvent = msgData.event;
} }

View File

@ -106,7 +106,7 @@ function getFilterName(userId: string, suffix?: string): string {
return `FILTER_SYNC_${userId}` + (suffix ? "_" + suffix : ""); return `FILTER_SYNC_${userId}` + (suffix ? "_" + suffix : "");
} }
function debuglog(...params): void { function debuglog(...params: any[]): void {
if (!DEBUG) return; if (!DEBUG) return;
logger.log(...params); logger.log(...params);
} }
@ -1093,7 +1093,7 @@ export class SyncApi {
// handle non-room account_data // handle non-room account_data
if (Array.isArray(data.account_data?.events)) { if (Array.isArray(data.account_data?.events)) {
const events = data.account_data.events.map(client.getEventMapper()); const events = data.account_data.events.map(client.getEventMapper());
const prevEventsMap = events.reduce((m, c) => { const prevEventsMap = events.reduce<Record<string, MatrixEvent | undefined>>((m, c) => {
m[c.getType()!] = client.store.getAccountData(c.getType()); m[c.getType()!] = client.store.getAccountData(c.getType());
return m; return m;
}, {}); }, {});
@ -1473,12 +1473,13 @@ export class SyncApi {
this.opts.crypto.updateOneTimeKeyCount(currentCount); this.opts.crypto.updateOneTimeKeyCount(currentCount);
} }
if (this.opts.crypto && if (this.opts.crypto &&
(data["device_unused_fallback_key_types"] || (data.device_unused_fallback_key_types ||
data["org.matrix.msc2732.device_unused_fallback_key_types"])) { data["org.matrix.msc2732.device_unused_fallback_key_types"])
) {
// The presence of device_unused_fallback_key_types indicates that the // The presence of device_unused_fallback_key_types indicates that the
// server supports fallback keys. If there's no unused // server supports fallback keys. If there's no unused
// signed_curve25519 fallback key we need a new one. // signed_curve25519 fallback key we need a new one.
const unusedFallbackKeys = data["device_unused_fallback_key_types"] || const unusedFallbackKeys = data.device_unused_fallback_key_types ||
data["org.matrix.msc2732.device_unused_fallback_key_types"]; data["org.matrix.msc2732.device_unused_fallback_key_types"];
this.opts.crypto.setNeedsNewFallback( this.opts.crypto.setNeedsNewFallback(
Array.isArray(unusedFallbackKeys) && Array.isArray(unusedFallbackKeys) &&
@ -1607,9 +1608,10 @@ export class SyncApi {
return []; return [];
} }
const mapper = this.client.getEventMapper({ decrypt }); const mapper = this.client.getEventMapper({ decrypt });
return (obj.events as Array<IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent>).map(function(e) { type TaggedEvent = (IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent) & { room_id?: string };
return (obj.events as TaggedEvent[]).map(function(e) {
if (room) { if (room) {
e["room_id"] = room.roomId; e.room_id = room.roomId;
} }
return mapper(e); return mapper(e);
}); });

View File

@ -316,7 +316,7 @@ export function deepSortedObjectEntries(obj: any): [string, any][] {
* @param {*} value the value to test * @param {*} value the value to test
* @return {boolean} whether or not value is a finite number without type-coercion * @return {boolean} whether or not value is a finite number without type-coercion
*/ */
export function isNumber(value: any): boolean { export function isNumber(value: any): value is number {
return typeof value === 'number' && isFinite(value); return typeof value === 'number' && isFinite(value);
} }
@ -428,8 +428,8 @@ export interface IDeferred<T> {
// Returns a Deferred // Returns a Deferred
export function defer<T = void>(): IDeferred<T> { export function defer<T = void>(): IDeferred<T> {
let resolve; let resolve!: IDeferred<T>["resolve"];
let reject; let reject!: IDeferred<T>["reject"];
const promise = new Promise<T>((_resolve, _reject) => { const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve; resolve = _resolve;
@ -665,18 +665,22 @@ export function compare(a: string, b: string): number {
* @param {Object} source * @param {Object} source
* @returns the target object * @returns the target object
*/ */
export function recursivelyAssign(target: Object, source: Object, ignoreNullish = false): any { export function recursivelyAssign<T1 extends T2, T2 extends Record<string, any>>(
target: T1,
source: T2,
ignoreNullish = false,
): T1 & T2 {
for (const [sourceKey, sourceValue] of Object.entries(source)) { for (const [sourceKey, sourceValue] of Object.entries(source)) {
if (target[sourceKey] instanceof Object && sourceValue) { if (target[sourceKey] instanceof Object && sourceValue) {
recursivelyAssign(target[sourceKey], sourceValue); recursivelyAssign(target[sourceKey], sourceValue);
continue; continue;
} }
if ((sourceValue !== null && sourceValue !== undefined) || !ignoreNullish) { if ((sourceValue !== null && sourceValue !== undefined) || !ignoreNullish) {
target[sourceKey] = sourceValue; target[sourceKey as keyof T1] = sourceValue;
continue; continue;
} }
} }
return target; return target as T1 & T2;
} }
function getContentTimestampWithFallback(event: MatrixEvent): number { function getContentTimestampWithFallback(event: MatrixEvent): number {

View File

@ -27,7 +27,7 @@ import { parse as parseSdp, write as writeSdp } from "sdp-transform";
import { logger } from '../logger'; import { logger } from '../logger';
import * as utils from '../utils'; import * as utils from '../utils';
import { MatrixEvent } from '../models/event'; import { IContent, MatrixEvent } from '../models/event';
import { EventType, ToDeviceMessageId } from '../@types/event'; import { EventType, ToDeviceMessageId } from '../@types/event';
import { RoomMember } from '../models/room-member'; import { RoomMember } from '../models/room-member';
import { randomString } from '../randomstring'; import { randomString } from '../randomstring';
@ -1101,7 +1101,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.terminate(CallParty.Local, reason, !suppressEvent); this.terminate(CallParty.Local, reason, !suppressEvent);
// We don't want to send hangup here if we didn't even get to sending an invite // We don't want to send hangup here if we didn't even get to sending an invite
if ([CallState.Fledgling, CallState.WaitLocalMedia].includes(this.state)) return; if ([CallState.Fledgling, CallState.WaitLocalMedia].includes(this.state)) return;
const content = {}; const content: IContent = {};
// Don't send UserHangup reason to older clients // Don't send UserHangup reason to older clients
if ((this.opponentVersion && this.opponentVersion !== 0) || reason !== CallErrorCode.UserHangup) { if ((this.opponentVersion && this.opponentVersion !== 0) || reason !== CallErrorCode.UserHangup) {
content["reason"] = reason; content["reason"] = reason;

View File

@ -5,7 +5,6 @@
"esModuleInterop": true, "esModuleInterop": true,
"module": "commonjs", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"noImplicitAny": false,
"noUnusedLocals": true, "noUnusedLocals": true,
"noEmit": true, "noEmit": true,
"declaration": true, "declaration": true,

View File

@ -1423,9 +1423,9 @@
dependencies: dependencies:
lodash "^4.17.21" lodash "^4.17.21"
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz": "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
version "3.2.13" version "3.2.14"
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz#0109fde93bcc61def851f79826c9384c073b5175" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984"
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3":
version "2.1.8-no-fsevents.3" version "2.1.8-no-fsevents.3"