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

Apply more strict typescript around the codebase (#2778)

* Apply more strict typescript around the codebase

* Fix tests

* Revert strict mode commit

* Iterate strict

* Iterate

* Iterate strict

* Iterate

* Fix tests

* Iterate

* Iterate strict

* Add tests

* Iterate

* Iterate

* Fix tests

* Fix tests

* Strict types be strict

* Fix types

* detectOpenHandles

* Strict

* Fix client not stopping

* Add sync peeking tests

* Make test happier

* More strict

* Iterate

* Stabilise

* Moar strictness

* Improve coverage

* Fix types

* Fix types

* Improve types further

* Fix types

* Improve typing of NamespacedValue

* Fix types
This commit is contained in:
Michael Telatynski
2022-10-21 11:44:40 +01:00
committed by GitHub
parent fdbbd9bca4
commit 867a0ca7ee
94 changed files with 1980 additions and 1735 deletions

View File

@ -38,8 +38,8 @@ import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
export class TestClient { export class TestClient {
public readonly httpBackend: MockHttpBackend; public readonly httpBackend: MockHttpBackend;
public readonly client: MatrixClient; public readonly client: MatrixClient;
public deviceKeys: IDeviceKeys; public deviceKeys?: IDeviceKeys | null;
public oneTimeKeys: Record<string, IOneTimeKey>; public oneTimeKeys?: Record<string, IOneTimeKey>;
constructor( constructor(
public readonly userId?: string, public readonly userId?: string,
@ -123,7 +123,7 @@ export class TestClient {
logger.log(this + ': received device keys'); logger.log(this + ': received device keys');
// we expect this to happen before any one-time keys are uploaded. // we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(this.oneTimeKeys).length).toEqual(0); expect(Object.keys(this.oneTimeKeys!).length).toEqual(0);
this.deviceKeys = content.device_keys; this.deviceKeys = content.device_keys;
return { one_time_key_counts: { signed_curve25519: 0 } }; return { one_time_key_counts: { signed_curve25519: 0 } };
@ -138,9 +138,9 @@ export class TestClient {
* @returns {Promise} for the one-time keys * @returns {Promise} for the one-time keys
*/ */
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> { public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
if (Object.keys(this.oneTimeKeys).length != 0) { if (Object.keys(this.oneTimeKeys!).length != 0) {
// already got one-time keys // already got one-time keys
return Promise.resolve(this.oneTimeKeys); return Promise.resolve(this.oneTimeKeys!);
} }
this.httpBackend.when("POST", "/keys/upload") this.httpBackend.when("POST", "/keys/upload")
@ -148,7 +148,7 @@ export class TestClient {
expect(content.device_keys).toBe(undefined); expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBe(undefined); expect(content.one_time_keys).toBe(undefined);
return { one_time_key_counts: { return { one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length, signed_curve25519: Object.keys(this.oneTimeKeys!).length,
} }; } };
}); });
@ -158,17 +158,17 @@ export class TestClient {
expect(content.one_time_keys).toBeTruthy(); expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({}); expect(content.one_time_keys).not.toEqual({});
logger.log('%s: received %i one-time keys', this, logger.log('%s: received %i one-time keys', this,
Object.keys(content.one_time_keys).length); Object.keys(content.one_time_keys!).length);
this.oneTimeKeys = content.one_time_keys; this.oneTimeKeys = content.one_time_keys;
return { one_time_key_counts: { return { one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length, signed_curve25519: Object.keys(this.oneTimeKeys!).length,
} }; } };
}); });
// this can take ages // this can take ages
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => { return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
expect(flushed).toEqual(2); expect(flushed).toEqual(2);
return this.oneTimeKeys; return this.oneTimeKeys!;
}); });
} }
@ -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![userId]).toEqual([]);
}); });
return response; return response;
}); });
@ -206,7 +206,7 @@ export class TestClient {
*/ */
public getDeviceKey(): string { public getDeviceKey(): string {
const keyId = 'curve25519:' + this.deviceId; const keyId = 'curve25519:' + this.deviceId;
return this.deviceKeys.keys[keyId]; return this.deviceKeys!.keys[keyId];
} }
/** /**
@ -216,7 +216,7 @@ export class TestClient {
*/ */
public getSigningKey(): string { public getSigningKey(): string {
const keyId = 'ed25519:' + this.deviceId; const keyId = 'ed25519:' + this.deviceId;
return this.deviceKeys.keys[keyId]; return this.deviceKeys!.keys[keyId];
} }
/** /**
@ -237,6 +237,6 @@ export class TestClient {
} }
public getUserId(): string { public getUserId(): string {
return this.userId; return this.userId!;
} }
} }

View File

@ -59,7 +59,7 @@ async function bobUploadsDeviceKeys(): Promise<void> {
bobTestClient.client.uploadKeys(), bobTestClient.client.uploadKeys(),
bobTestClient.httpBackend.flushAllExpected(), bobTestClient.httpBackend.flushAllExpected(),
]); ]);
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0); expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
} }
/** /**
@ -99,7 +99,7 @@ async function expectAliClaimKeys(): Promise<void> {
expect(claimType).toEqual("signed_curve25519"); expect(claimType).toEqual("signed_curve25519");
let keyId = ''; let keyId = '';
for (keyId in keys) { for (keyId in keys) {
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) { if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) { if (keyId.indexOf(claimType + ":") === 0) {
break; break;
} }
@ -137,7 +137,7 @@ async function aliDownloadsKeys(): Promise<void> {
// @ts-ignore - protected // @ts-ignore - protected
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data!.devices[bobUserId]!; const devices = data!.devices[bobUserId]!;
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys); expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys);
expect(devices[bobDeviceId].verified). expect(devices[bobDeviceId].verified).
toBe(DeviceInfo.DeviceVerification.UNVERIFIED); toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
}); });
@ -223,7 +223,7 @@ async function expectBobSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(bobTestClient.httpBackend); const content = await expectSendMessageRequest(bobTestClient.httpBackend);
bobMessages.push(content); bobMessages.push(content);
const aliKeyId = "curve25519:" + aliDeviceId; const aliKeyId = "curve25519:" + aliDeviceId;
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId]; const aliDeviceCurve25519Key = aliTestClient.deviceKeys!.keys[aliKeyId];
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]); expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
const ciphertext = content.ciphertext[aliDeviceCurve25519Key]; const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeTruthy(); expect(ciphertext).toBeTruthy();
@ -393,7 +393,7 @@ describe("MatrixClient crypto", () => {
it("Ali gets keys with an invalid signature", async () => { it("Ali gets keys with an invalid signature", async () => {
await bobUploadsDeviceKeys(); await bobUploadsDeviceKeys();
// tamper bob's keys // tamper bob's keys
const bobDeviceKeys = bobTestClient.deviceKeys; const bobDeviceKeys = bobTestClient.deviceKeys!;
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy(); expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc"; bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
await Promise.all([ await Promise.all([
@ -479,7 +479,7 @@ describe("MatrixClient crypto", () => {
await bobTestClient.start(); await bobTestClient.start();
const keys = await bobTestClient.awaitOneTimeKeyUpload(); const keys = await bobTestClient.awaitOneTimeKeyUpload();
expect(Object.keys(keys).length).toEqual(5); expect(Object.keys(keys).length).toEqual(5);
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0); expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
}); });
it("Ali sends a message", async () => { it("Ali sends a message", async () => {

View File

@ -1047,7 +1047,7 @@ describe("MatrixClient event timelines", function() {
response = { response = {
chunk: [THREAD_ROOT], chunk: [THREAD_ROOT],
state: [], state: [],
next_batch: RANDOM_TOKEN, next_batch: RANDOM_TOKEN as string | null,
}, },
): ExpectedHttpRequest { ): ExpectedHttpRequest {
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", { const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", {

View File

@ -139,7 +139,7 @@ describe("MatrixClient", function() {
const r = client!.cancelUpload(prom); const r = client!.cancelUpload(prom);
expect(r).toBe(true); expect(r).toBe(true);
await expect(prom).rejects.toThrow("Aborted"); await expect(prom).rejects.toThrow("Aborted");
expect(client.getCurrentUploads()).toHaveLength(0); expect(client!.getCurrentUploads()).toHaveLength(0);
}); });
}); });
@ -178,7 +178,7 @@ describe("MatrixClient", function() {
expect(request.data.third_party_signed).toEqual(signature); expect(request.data.third_party_signed).toEqual(signature);
}).respond(200, { room_id: roomId }); }).respond(200, { room_id: roomId });
const prom = client.joinRoom(roomId, { const prom = client!.joinRoom(roomId, {
inviteSignUrl, inviteSignUrl,
viaServers, viaServers,
}); });
@ -1164,18 +1164,18 @@ describe("MatrixClient", function() {
describe("logout", () => { describe("logout", () => {
it("should abort pending requests when called with stopClient=true", async () => { it("should abort pending requests when called with stopClient=true", async () => {
httpBackend.when("POST", "/logout").respond(200, {}); httpBackend!.when("POST", "/logout").respond(200, {});
const fn = jest.fn(); const fn = jest.fn();
client.http.request(Method.Get, "/test").catch(fn); client!.http.request(Method.Get, "/test").catch(fn);
client.logout(true); client!.logout(true);
await httpBackend.flush(undefined); await httpBackend!.flush(undefined);
expect(fn).toHaveBeenCalled(); expect(fn).toHaveBeenCalled();
}); });
}); });
describe("sendHtmlEmote", () => { describe("sendHtmlEmote", () => {
it("should send valid html emote", async () => { it("should send valid html emote", async () => {
httpBackend.when("PUT", "/send").check(req => { httpBackend!.when("PUT", "/send").check(req => {
expect(req.data).toStrictEqual({ expect(req.data).toStrictEqual({
"msgtype": "m.emote", "msgtype": "m.emote",
"body": "Body", "body": "Body",
@ -1184,15 +1184,15 @@ describe("MatrixClient", function() {
"org.matrix.msc1767.message": expect.anything(), "org.matrix.msc1767.message": expect.anything(),
}); });
}).respond(200, { event_id: "$foobar" }); }).respond(200, { event_id: "$foobar" });
const prom = client.sendHtmlEmote("!room:server", "Body", "<h1>Body</h1>"); const prom = client!.sendHtmlEmote("!room:server", "Body", "<h1>Body</h1>");
await httpBackend.flush(undefined); await httpBackend!.flush(undefined);
await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" }); await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" });
}); });
}); });
describe("sendHtmlMessage", () => { describe("sendHtmlMessage", () => {
it("should send valid html message", async () => { it("should send valid html message", async () => {
httpBackend.when("PUT", "/send").check(req => { httpBackend!.when("PUT", "/send").check(req => {
expect(req.data).toStrictEqual({ expect(req.data).toStrictEqual({
"msgtype": "m.text", "msgtype": "m.text",
"body": "Body", "body": "Body",
@ -1201,24 +1201,24 @@ describe("MatrixClient", function() {
"org.matrix.msc1767.message": expect.anything(), "org.matrix.msc1767.message": expect.anything(),
}); });
}).respond(200, { event_id: "$foobar" }); }).respond(200, { event_id: "$foobar" });
const prom = client.sendHtmlMessage("!room:server", "Body", "<h1>Body</h1>"); const prom = client!.sendHtmlMessage("!room:server", "Body", "<h1>Body</h1>");
await httpBackend.flush(undefined); await httpBackend!.flush(undefined);
await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" }); await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" });
}); });
}); });
describe("forget", () => { describe("forget", () => {
it("should remove from store by default", async () => { it("should remove from store by default", async () => {
const room = new Room("!roomId:server", client, userId); const room = new Room("!roomId:server", client!, userId);
client.store.storeRoom(room); client!.store.storeRoom(room);
expect(client.store.getRooms()).toContain(room); expect(client!.store.getRooms()).toContain(room);
httpBackend.when("POST", "/forget").respond(200, {}); httpBackend!.when("POST", "/forget").respond(200, {});
await Promise.all([ await Promise.all([
client.forget(room.roomId), client!.forget(room.roomId),
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
]); ]);
expect(client.store.getRooms()).not.toContain(room); expect(client!.store.getRooms()).not.toContain(room);
}); });
}); });
@ -1306,8 +1306,8 @@ describe("MatrixClient", function() {
const resp = await prom; const resp = await prom;
expect(resp.access_token).toBe(token); expect(resp.access_token).toBe(token);
expect(resp.user_id).toBe(userId); expect(resp.user_id).toBe(userId);
expect(client.getUserId()).toBe(userId); expect(client!.getUserId()).toBe(userId);
expect(client.http.opts.accessToken).toBe(token); expect(client!.http.opts.accessToken).toBe(token);
}); });
}); });

View File

@ -1541,6 +1541,67 @@ describe("MatrixClient syncing", () => {
}); });
}); });
describe("peek", () => {
beforeEach(() => {
httpBackend!.expectedRequests = [];
});
it("should return a room based on the room initialSync API", async () => {
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomOne)}/initialSync`).respond(200, {
room_id: roomOne,
membership: "leave",
messages: {
start: "start",
end: "end",
chunk: [{
content: { body: "Message 1" },
type: "m.room.message",
event_id: "$eventId1",
sender: userA,
origin_server_ts: 12313525,
room_id: roomOne,
}, {
content: { body: "Message 2" },
type: "m.room.message",
event_id: "$eventId2",
sender: userB,
origin_server_ts: 12315625,
room_id: roomOne,
}],
},
state: [{
content: { name: "Room Name" },
type: "m.room.name",
event_id: "$eventId",
sender: userA,
origin_server_ts: 12314525,
state_key: "",
room_id: roomOne,
}],
presence: [{
content: {},
type: "m.presence",
sender: userA,
}],
});
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
const prom = client!.peekInRoom(roomOne);
await httpBackend!.flushAllExpected();
const room = await prom;
expect(room.roomId).toBe(roomOne);
expect(room.getMyMembership()).toBe("leave");
expect(room.name).toBe("Room Name");
expect(room.currentState.getStateEvents("m.room.name", "").getId()).toBe("$eventId");
expect(room.timeline[0].getContent().body).toBe("Message 1");
expect(room.timeline[1].getContent().body).toBe("Message 2");
client?.stopPeeking();
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
await httpBackend!.flushAllExpected();
});
});
/** /**
* waits for the MatrixClient to emit one or more 'sync' events. * waits for the MatrixClient to emit one or more 'sync' events.
* *

View File

@ -1160,11 +1160,11 @@ describe("megolm", () => {
"algorithm": 'm.megolm.v1.aes-sha2', "algorithm": 'm.megolm.v1.aes-sha2',
"room_id": ROOM_ID, "room_id": ROOM_ID,
"sender_key": content.sender_key, "sender_key": content.sender_key,
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key, "sender_claimed_ed25519_key": groupSessionKey!.sender_claimed_ed25519_key,
"session_id": content.session_id, "session_id": content.session_id,
"session_key": groupSessionKey.key, "session_key": groupSessionKey!.key,
"chain_index": groupSessionKey.chain_index, "chain_index": groupSessionKey!.chain_index,
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain, "forwarding_curve25519_key_chain": groupSessionKey!.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": true, "org.matrix.msc3061.shared_history": true,
}, },
plaintype: 'm.forwarded_room_key', plaintype: 'm.forwarded_room_key',
@ -1298,11 +1298,11 @@ describe("megolm", () => {
"algorithm": 'm.megolm.v1.aes-sha2', "algorithm": 'm.megolm.v1.aes-sha2',
"room_id": ROOM_ID, "room_id": ROOM_ID,
"sender_key": content.sender_key, "sender_key": content.sender_key,
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key, "sender_claimed_ed25519_key": groupSessionKey!.sender_claimed_ed25519_key,
"session_id": content.session_id, "session_id": content.session_id,
"session_key": groupSessionKey.key, "session_key": groupSessionKey!.key,
"chain_index": groupSessionKey.chain_index, "chain_index": groupSessionKey!.chain_index,
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain, "forwarding_curve25519_key_chain": groupSessionKey!.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": true, "org.matrix.msc3061.shared_history": true,
}, },
plaintype: 'm.forwarded_room_key', plaintype: 'm.forwarded_room_key',

View File

@ -468,7 +468,7 @@ describe("SlidingSyncSdk", () => {
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => { it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
mockSlidingSync!.emit( mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
{ pos: "h", lists: [], rooms: {}, extensions: {} }, null, { pos: "h", lists: [], rooms: {}, extensions: {} },
); );
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing); expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
@ -490,7 +490,6 @@ describe("SlidingSyncSdk", () => {
SlidingSyncEvent.Lifecycle, SlidingSyncEvent.Lifecycle,
SlidingSyncState.Complete, SlidingSyncState.Complete,
{ pos: "i", lists: [], rooms: {}, extensions: {} }, { pos: "i", lists: [], rooms: {}, extensions: {} },
null,
); );
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing); expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
}); });

View File

@ -82,7 +82,7 @@ describe("SlidingSync", () => {
it("should reset the connection on HTTP 400 and send everything again", async () => { it("should reset the connection on HTTP 400 and send everything again", async () => {
// seed the connection with some lists, extensions and subscriptions to verify they are sent again // seed the connection with some lists, extensions and subscriptions to verify they are sent again
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1); slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
const roomId = "!sub:localhost"; const roomId = "!sub:localhost";
const subInfo = { const subInfo = {
timeline_limit: 42, timeline_limit: 42,
@ -108,7 +108,7 @@ describe("SlidingSync", () => {
// expect everything to be sent // expect everything to be sent
let txnId; let txnId;
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);
expect(body.room_subscriptions).toEqual({ expect(body.room_subscriptions).toEqual({
@ -117,7 +117,7 @@ describe("SlidingSync", () => {
expect(body.lists[0]).toEqual(listInfo); expect(body.lists[0]).toEqual(listInfo);
expect(body.extensions).toBeTruthy(); expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: true }); expect(body.extensions["custom_extension"]).toEqual({ initial: true });
expect(req.queryParams["pos"]).toBeUndefined(); expect(req.queryParams!["pos"]).toBeUndefined();
txnId = body.txn_id; txnId = body.txn_id;
}).respond(200, function() { }).respond(200, function() {
return { return {
@ -127,10 +127,10 @@ describe("SlidingSync", () => {
txn_id: txnId, txn_id: txnId,
}; };
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
// expect nothing but ranges and non-initial extensions to be sent // expect nothing but ranges and non-initial extensions to be sent
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);
expect(body.room_subscriptions).toBeFalsy(); expect(body.room_subscriptions).toBeFalsy();
@ -139,7 +139,7 @@ describe("SlidingSync", () => {
}); });
expect(body.extensions).toBeTruthy(); expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: false }); expect(body.extensions["custom_extension"]).toEqual({ initial: false });
expect(req.queryParams["pos"]).toEqual("11"); expect(req.queryParams!["pos"]).toEqual("11");
}).respond(200, function() { }).respond(200, function() {
return { return {
pos: "12", pos: "12",
@ -147,19 +147,19 @@ describe("SlidingSync", () => {
extensions: {}, extensions: {},
}; };
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
// now we expire the session // now we expire the session
httpBackend.when("POST", syncUrl).respond(400, function() { httpBackend!.when("POST", syncUrl).respond(400, function() {
logger.debug("sending session expired 400"); logger.debug("sending session expired 400");
return { return {
error: "HTTP 400 : session expired", error: "HTTP 400 : session expired",
}; };
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
// ...and everything should be sent again // ...and everything should be sent again
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);
expect(body.room_subscriptions).toEqual({ expect(body.room_subscriptions).toEqual({
@ -168,7 +168,7 @@ describe("SlidingSync", () => {
expect(body.lists[0]).toEqual(listInfo); expect(body.lists[0]).toEqual(listInfo);
expect(body.extensions).toBeTruthy(); expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: true }); expect(body.extensions["custom_extension"]).toEqual({ initial: true });
expect(req.queryParams["pos"]).toBeUndefined(); expect(req.queryParams!["pos"]).toBeUndefined();
}).respond(200, function() { }).respond(200, function() {
return { return {
pos: "1", pos: "1",
@ -176,7 +176,7 @@ describe("SlidingSync", () => {
extensions: {}, extensions: {},
}; };
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
slidingSync.stop(); slidingSync.stop();
}); });
}); });
@ -415,7 +415,7 @@ describe("SlidingSync", () => {
expect(slidingSync.getList(0)).toBeDefined(); expect(slidingSync.getList(0)).toBeDefined();
expect(slidingSync.getList(5)).toBeNull(); expect(slidingSync.getList(5)).toBeNull();
expect(slidingSync.getListData(5)).toBeNull(); expect(slidingSync.getListData(5)).toBeNull();
const syncData = slidingSync.getListData(0); const syncData = slidingSync.getListData(0)!;
expect(syncData.joinedCount).toEqual(500); // from previous test expect(syncData.joinedCount).toEqual(500); // from previous test
expect(syncData.roomIndexToRoomId).toEqual({ expect(syncData.roomIndexToRoomId).toEqual({
0: roomA, 0: roomA,
@ -665,7 +665,7 @@ describe("SlidingSync", () => {
0: roomB, 0: roomB,
1: roomC, 1: roomC,
}; };
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId); expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual(indexToRoomId);
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "f", pos: "f",
// currently the list is [B,C] so we will insert D then immediately delete it // currently the list is [B,C] so we will insert D then immediately delete it
@ -703,7 +703,7 @@ describe("SlidingSync", () => {
}); });
it("should handle deletions correctly", async () => { it("should handle deletions correctly", async () => {
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({ expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
0: roomB, 0: roomB,
1: roomC, 1: roomC,
}); });
@ -739,7 +739,7 @@ describe("SlidingSync", () => {
}); });
it("should handle insertions correctly", async () => { it("should handle insertions correctly", async () => {
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({ expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
0: roomC, 0: roomC,
}); });
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {

View File

@ -135,7 +135,7 @@ export class MockMediaDeviceInfo {
export class MockMediaHandler { export class MockMediaHandler {
getUserMediaStream(audio: boolean, video: boolean) { getUserMediaStream(audio: boolean, video: boolean) {
const tracks = []; const tracks: MockMediaStreamTrack[] = [];
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio")); if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video")); if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));

View File

@ -32,7 +32,7 @@ describe("NamespacedValue", () => {
}); });
it("should have a falsey unstable if needed", () => { it("should have a falsey unstable if needed", () => {
const ns = new NamespacedValue("stable", null); const ns = new NamespacedValue("stable");
expect(ns.name).toBe(ns.stable); expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBeFalsy(); expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.stable]); expect(ns.names).toEqual([ns.stable]);
@ -41,17 +41,17 @@ describe("NamespacedValue", () => {
it("should match against either stable or unstable", () => { it("should match against either stable or unstable", () => {
const ns = new NamespacedValue("stable", "unstable"); const ns = new NamespacedValue("stable", "unstable");
expect(ns.matches("no")).toBe(false); expect(ns.matches("no")).toBe(false);
expect(ns.matches(ns.stable)).toBe(true); expect(ns.matches(ns.stable!)).toBe(true);
expect(ns.matches(ns.unstable)).toBe(true); expect(ns.matches(ns.unstable!)).toBe(true);
}); });
it("should not permit falsey values for both parts", () => { it("should not permit falsey values for both parts", () => {
try { try {
new UnstableValue(null, null); new UnstableValue(null!, null!);
// noinspection ExceptionCaughtLocallyJS // noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail"); throw new Error("Failed to fail");
} catch (e) { } catch (e) {
expect(e.message).toBe("One of stable or unstable values must be supplied"); expect((<Error>e).message).toBe("One of stable or unstable values must be supplied");
} }
}); });
}); });
@ -65,7 +65,7 @@ describe("UnstableValue", () => {
}); });
it("should return unstable if there is no stable", () => { it("should return unstable if there is no stable", () => {
const ns = new UnstableValue(null, "unstable"); const ns = new UnstableValue(null!, "unstable");
expect(ns.name).toBe(ns.unstable); expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy(); expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]); expect(ns.names).toEqual([ns.unstable]);
@ -73,11 +73,11 @@ describe("UnstableValue", () => {
it("should not permit falsey unstable values", () => { it("should not permit falsey unstable values", () => {
try { try {
new UnstableValue("stable", null); new UnstableValue("stable", null!);
// noinspection ExceptionCaughtLocallyJS // noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail"); throw new Error("Failed to fail");
} catch (e) { } catch (e) {
expect(e.message).toBe("Unstable value must be supplied"); expect((<Error>e).message).toBe("Unstable value must be supplied");
} }
}); });
}); });

View File

@ -678,7 +678,7 @@ describe("AutoDiscovery", function() {
it("should return FAIL_PROMPT for connection errors", () => { it("should return FAIL_PROMPT for connection errors", () => {
const httpBackend = getHttpBackend(); const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined); httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined!);
return Promise.all([ return Promise.all([
httpBackend.flushAllExpected(), httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => { AutoDiscovery.findClientConfig("example.org").then((conf) => {

View File

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

View File

@ -2,6 +2,7 @@ import '../olm-loader';
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
import { MatrixClient } from "../../src/client"; import { 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";
@ -32,7 +33,7 @@ function awaitEvent(emitter, event) {
async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent> { async function keyshareEventForEvent(client, event, index): 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,
eventContent.sender_key, eventContent.sender_key,
eventContent.session_id, eventContent.session_id,
@ -68,10 +69,10 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixEvent { function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixEvent {
const roomId = event.getRoomId(); const roomId = event.getRoomId();
const eventContent = event.getWireContent(); const eventContent = event.getWireContent();
const key = client.crypto.olmDevice.getOutboundGroupSessionKey(eventContent.session_id); const key = client.crypto!.olmDevice.getOutboundGroupSessionKey(eventContent.session_id);
const ksEvent = new MatrixEvent({ const ksEvent = new MatrixEvent({
type: "m.room_key", type: "m.room_key",
sender: client.getUserId(), sender: client.getUserId()!,
content: { content: {
"algorithm": olmlib.MEGOLM_ALGORITHM, "algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": roomId, "room_id": roomId,
@ -146,7 +147,7 @@ describe("Crypto", function() {
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI'; 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
device.keys["ed25519:FLIBBLE"] = device.keys["ed25519:FLIBBLE"] =
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
client.crypto.deviceList.getDeviceByIdentityKey = () => device; client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
encryptionInfo = client.getEventEncryptionInfo(event); encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy(); expect(encryptionInfo.encrypted).toBeTruthy();
@ -334,7 +335,7 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => { await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt // alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending // them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom); await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event // remove keys from the event
// @ts-ignore private properties // @ts-ignore private properties
event.clearEvent = undefined; event.clearEvent = undefined;
@ -343,17 +344,17 @@ describe("Crypto", function() {
// @ts-ignore private properties // @ts-ignore private properties
event.claimedEd25519Key = null; event.claimedEd25519Key = null;
try { try {
await bobClient.crypto.decryptEvent(event); await bobClient.crypto!.decryptEvent(event);
} catch (e) { } catch (e) {
// we expect this to fail because we don't have the // we expect this to fail because we don't have the
// decryption keys yet // decryption keys yet
} }
})); }));
const device = new DeviceInfo(aliceClient.deviceId); const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
const bobDecryptor = bobClient.crypto.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
@ -365,14 +366,14 @@ describe("Crypto", function() {
// the first message can't be decrypted yet, but the second one // the first message can't be decrypted yet, but the second one
// can // can
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1); let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
bobClient.crypto.deviceList.downloadKeys = () => Promise.resolve({}); bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com"; bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
await bobDecryptor.onRoomKeyEvent(ksEvent); await bobDecryptor.onRoomKeyEvent(ksEvent);
await decryptEventsPromise; await decryptEventsPromise;
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted"); expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted"); expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
const cryptoStore = bobClient.crypto.cryptoStore; const cryptoStore = bobClient.crypto!.cryptoStore;
const eventContent = events[0].getWireContent(); const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key; const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id; const sessionId = eventContent.session_id;
@ -437,7 +438,7 @@ describe("Crypto", function() {
}); });
// alice encrypts each event, and then bob tries to decrypt // alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending // them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom); await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event // remove keys from the event
// @ts-ignore private property // @ts-ignore private property
event.clearEvent = undefined; event.clearEvent = undefined;
@ -446,24 +447,24 @@ describe("Crypto", function() {
// @ts-ignore private property // @ts-ignore private property
event.claimedEd25519Key = null; event.claimedEd25519Key = null;
try { try {
await bobClient.crypto.decryptEvent(event); await bobClient.crypto!.decryptEvent(event);
} catch (e) { } catch (e) {
// we expect this to fail because we don't have the // we expect this to fail because we don't have the
// decryption keys yet // decryption keys yet
} }
const device = new DeviceInfo(aliceClient.deviceId); const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
const bobDecryptor = bobClient.crypto.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
const ksEvent = await keyshareEventForEvent(aliceClient, event, 1); const ksEvent = await keyshareEventForEvent(aliceClient, event, 1);
ksEvent.getContent().sender_key = undefined; // test ksEvent.getContent().sender_key = undefined; // test
bobClient.crypto.olmDevice.addInboundGroupSession = jest.fn(); bobClient.crypto!.olmDevice.addInboundGroupSession = jest.fn();
await bobDecryptor.onRoomKeyEvent(ksEvent); await bobDecryptor.onRoomKeyEvent(ksEvent);
expect(bobClient.crypto.olmDevice.addInboundGroupSession).not.toHaveBeenCalled(); expect(bobClient.crypto!.olmDevice.addInboundGroupSession).not.toHaveBeenCalled();
}); });
it("creates a new keyshare request if we request a keyshare", async function() { it("creates a new keyshare request if we request a keyshare", async function() {
@ -479,7 +480,7 @@ describe("Crypto", function() {
}, },
}); });
await aliceClient.cancelAndResendEventRoomKeyRequest(event); await aliceClient.cancelAndResendEventRoomKeyRequest(event);
const cryptoStore = aliceClient.crypto.cryptoStore; const cryptoStore = aliceClient.crypto!.cryptoStore;
const roomKeyRequestBody = { const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM, algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: "!someroom", room_id: "!someroom",
@ -514,7 +515,7 @@ describe("Crypto", function() {
// let the client set up enough for that to happen, so gut-wrench a bit // let the client set up enough for that to happen, so gut-wrench a bit
// to force it to send now. // to force it to send now.
// @ts-ignore // @ts-ignore
aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests(); aliceClient.crypto!.outgoingRoomKeyRequestManager.sendQueuedRequests();
jest.runAllTimers(); jest.runAllTimers();
await Promise.resolve(); await Promise.resolve();
expect(aliceSendToDevice).toBeCalledTimes(1); expect(aliceSendToDevice).toBeCalledTimes(1);
@ -571,7 +572,7 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => { await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt // alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending // them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom); await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event // remove keys from the event
// @ts-ignore private properties // @ts-ignore private properties
event.clearEvent = undefined; event.clearEvent = undefined;
@ -580,18 +581,18 @@ describe("Crypto", function() {
// @ts-ignore private properties // @ts-ignore private properties
event.claimedEd25519Key = null; event.claimedEd25519Key = null;
try { try {
await bobClient.crypto.decryptEvent(event); await bobClient.crypto!.decryptEvent(event);
} catch (e) { } catch (e) {
// we expect this to fail because we don't have the // we expect this to fail because we don't have the
// decryption keys yet // decryption keys yet
} }
})); }));
const device = new DeviceInfo(aliceClient.deviceId); const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com"; bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const cryptoStore = bobClient.crypto.cryptoStore; const cryptoStore = bobClient.crypto!.cryptoStore;
const eventContent = events[0].getWireContent(); const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key; const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id; const sessionId = eventContent.session_id;
@ -604,11 +605,11 @@ describe("Crypto", function() {
const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody); const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
expect(outgoingReq).toBeDefined(); expect(outgoingReq).toBeDefined();
await cryptoStore.updateOutgoingRoomKeyRequest( await cryptoStore.updateOutgoingRoomKeyRequest(
outgoingReq.requestId, RoomKeyRequestState.Unsent, outgoingReq!.requestId, RoomKeyRequestState.Unsent,
{ state: RoomKeyRequestState.Sent }, { state: RoomKeyRequestState.Sent },
); );
const bobDecryptor = bobClient.crypto.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
@ -617,7 +618,7 @@ describe("Crypto", function() {
})); }));
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent); await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey( const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId, roomId,
events[0].getWireContent().sender_key, events[0].getWireContent().sender_key,
events[0].getWireContent().session_id, events[0].getWireContent().session_id,
@ -675,7 +676,7 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => { await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt // alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending // them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom); await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event // remove keys from the event
// @ts-ignore private properties // @ts-ignore private properties
event.clearEvent = undefined; event.clearEvent = undefined;
@ -684,18 +685,18 @@ describe("Crypto", function() {
// @ts-ignore private properties // @ts-ignore private properties
event.claimedEd25519Key = null; event.claimedEd25519Key = null;
try { try {
await bobClient.crypto.decryptEvent(event); await bobClient.crypto!.decryptEvent(event);
} catch (e) { } catch (e) {
// we expect this to fail because we don't have the // we expect this to fail because we don't have the
// decryption keys yet // decryption keys yet
} }
})); }));
const device = new DeviceInfo(claraClient.deviceId); const device = new DeviceInfo(claraClient.deviceId!);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@clara:example.com"; bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@clara:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
@ -703,10 +704,10 @@ describe("Crypto", function() {
return awaitEvent(ev, "Event.decrypted"); return awaitEvent(ev, "Event.decrypted");
})); }));
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = claraClient.getUserId(), ksEvent.event.sender = claraClient.getUserId()!;
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()); ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!);
await bobDecryptor.onRoomKeyEvent(ksEvent); await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey( const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId, roomId,
events[0].getWireContent().sender_key, events[0].getWireContent().sender_key,
events[0].getWireContent().session_id, events[0].getWireContent().session_id,
@ -753,7 +754,7 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => { await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt // alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending // them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom); await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event // remove keys from the event
// @ts-ignore private properties // @ts-ignore private properties
event.clearEvent = undefined; event.clearEvent = undefined;
@ -762,19 +763,19 @@ describe("Crypto", function() {
// @ts-ignore private properties // @ts-ignore private properties
event.claimedEd25519Key = null; event.claimedEd25519Key = null;
try { try {
await bobClient.crypto.decryptEvent(event); await bobClient.crypto!.decryptEvent(event);
} catch (e) { } catch (e) {
// we expect this to fail because we don't have the // we expect this to fail because we don't have the
// decryption keys yet // decryption keys yet
} }
})); }));
const device = new DeviceInfo(claraClient.deviceId); const device = new DeviceInfo(claraClient.deviceId!);
device.verified = DeviceInfo.DeviceVerification.VERIFIED; device.verified = DeviceInfo.DeviceVerification.VERIFIED;
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:example.com"; bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@bob:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
@ -782,10 +783,10 @@ describe("Crypto", function() {
return awaitEvent(ev, "Event.decrypted"); return awaitEvent(ev, "Event.decrypted");
})); }));
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = bobClient.getUserId(), ksEvent.event.sender = bobClient.getUserId()!;
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()); ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()!);
await bobDecryptor.onRoomKeyEvent(ksEvent); await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey( const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId, roomId,
events[0].getWireContent().sender_key, events[0].getWireContent().sender_key,
events[0].getWireContent().session_id, events[0].getWireContent().session_id,
@ -835,7 +836,7 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => { await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt // alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending // them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom); await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event // remove keys from the event
// @ts-ignore private properties // @ts-ignore private properties
event.clearEvent = undefined; event.clearEvent = undefined;
@ -844,26 +845,26 @@ describe("Crypto", function() {
// @ts-ignore private properties // @ts-ignore private properties
event.claimedEd25519Key = null; event.claimedEd25519Key = null;
try { try {
await bobClient.crypto.decryptEvent(event); await bobClient.crypto!.decryptEvent(event);
} catch (e) { } catch (e) {
// we expect this to fail because we don't have the // we expect this to fail because we don't have the
// decryption keys yet // decryption keys yet
} }
})); }));
const device = new DeviceInfo(claraClient.deviceId); const device = new DeviceInfo(claraClient.deviceId!);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com"; bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = claraClient.getUserId(), ksEvent.event.sender = claraClient.getUserId()!;
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()); ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!);
await bobDecryptor.onRoomKeyEvent(ksEvent); await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey( const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId, roomId,
events[0].getWireContent().sender_key, events[0].getWireContent().sender_key,
events[0].getWireContent().session_id, events[0].getWireContent().session_id,
@ -904,7 +905,7 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => { await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt // alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending // them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom); await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event // remove keys from the event
// @ts-ignore private properties // @ts-ignore private properties
event.clearEvent = undefined; event.clearEvent = undefined;
@ -914,11 +915,11 @@ describe("Crypto", function() {
event.claimedEd25519Key = null; event.claimedEd25519Key = null;
})); }));
const device = new DeviceInfo(aliceClient.deviceId); const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com"; bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
@ -926,25 +927,25 @@ describe("Crypto", function() {
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent); await bobDecryptor.onRoomKeyEvent(ksEvent);
const bobKey = await bobClient.crypto.olmDevice.getInboundGroupSessionKey( const bobKey = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId, roomId,
content.sender_key, content.sender_key,
content.session_id, content.session_id,
); );
expect(bobKey).toBeNull(); expect(bobKey).toBeNull();
const aliceKey = await aliceClient.crypto.olmDevice.getInboundGroupSessionKey( const aliceKey = await aliceClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId, roomId,
content.sender_key, content.sender_key,
content.session_id, content.session_id,
); );
const parked = await bobClient.crypto.cryptoStore.takeParkedSharedHistory(roomId); const parked = await bobClient.crypto!.cryptoStore.takeParkedSharedHistory(roomId);
expect(parked).toEqual([{ expect(parked).toEqual([{
senderId: aliceClient.getUserId(), senderId: aliceClient.getUserId(),
senderKey: content.sender_key, senderKey: content.sender_key,
sessionId: content.session_id, sessionId: content.session_id,
sessionKey: aliceKey.key, sessionKey: aliceKey!.key,
keysClaimed: { ed25519: aliceKey.sender_claimed_ed25519_key }, keysClaimed: { ed25519: aliceKey!.sender_claimed_ed25519_key },
forwardingCurve25519KeyChain: ["akey"], forwardingCurve25519KeyChain: ["akey"],
}]); }]);
}); });
@ -956,19 +957,19 @@ describe("Crypto", function() {
jest.setTimeout(10000); jest.setTimeout(10000);
const client = (new TestClient("@a:example.com", "dev")).client; const client = (new TestClient("@a:example.com", "dev")).client;
await client.initCrypto(); await client.initCrypto();
client.crypto.getSecretStorageKey = jest.fn().mockResolvedValue(null); client.crypto!.getSecretStorageKey = jest.fn().mockResolvedValue(null);
client.crypto.isCrossSigningReady = async () => false; client.crypto!.isCrossSigningReady = async () => false;
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null); client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
client.crypto.baseApis.setAccountData = jest.fn().mockResolvedValue(null); client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
client.crypto.baseApis.uploadKeySignatures = jest.fn(); client.crypto!.baseApis.uploadKeySignatures = jest.fn();
client.crypto.baseApis.http.authedRequest = jest.fn(); client.crypto!.baseApis.http.authedRequest = jest.fn();
const createSecretStorageKey = async () => { const createSecretStorageKey = async () => {
return { return {
keyInfo: undefined, // Returning undefined here used to cause a crash keyInfo: undefined, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33), privateKey: Uint8Array.of(32, 33),
}; };
}; };
await client.crypto.bootstrapSecretStorage({ await client.crypto!.bootstrapSecretStorage({
createSecretStorageKey, createSecretStorageKey,
}); });
client.stopClient(); client.stopClient();
@ -995,7 +996,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: JSON.stringify(payload) },
}; };
}); });
@ -1075,4 +1076,50 @@ describe("Crypto", function() {
client.httpBackend.verifyNoOutstandingRequests(); client.httpBackend.verifyNoOutstandingRequests();
}); });
}); });
describe("checkSecretStoragePrivateKey", () => {
let client: TestClient;
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
});
afterEach(async () => {
await client.stop();
});
it("should free PkDecryption", () => {
const free = jest.fn();
jest.spyOn(Olm, "PkDecryption").mockImplementation(() => ({
init_with_private_key: jest.fn(),
free,
}) as unknown as PkDecryption);
client.client.checkSecretStoragePrivateKey(new Uint8Array(), "");
expect(free).toHaveBeenCalled();
});
});
describe("checkCrossSigningPrivateKey", () => {
let client: TestClient;
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
});
afterEach(async () => {
await client.stop();
});
it("should free PkSigning", () => {
const free = jest.fn();
jest.spyOn(Olm, "PkSigning").mockImplementation(() => ({
init_with_seed: jest.fn(),
free,
}) as unknown as PkSigning);
client.client.checkCrossSigningPrivateKey(new Uint8Array(), "");
expect(free).toHaveBeenCalled();
});
});
}); });

View File

@ -247,14 +247,14 @@ describe.each([
const olmDevice = new OlmDevice(store); const olmDevice = new OlmDevice(store);
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } = const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
createCryptoStoreCacheCallbacks(store, olmDevice); createCryptoStoreCacheCallbacks(store, olmDevice);
await storeCrossSigningKeyCache("self_signing", testKey); await storeCrossSigningKeyCache!("self_signing", testKey);
// If we've not saved anything, don't expect anything // If we've not saved anything, don't expect anything
// Definitely don't accidentally return the wrong key for the type // Definitely don't accidentally return the wrong key for the type
const nokey = await getCrossSigningKeyCache("self", ""); const nokey = await getCrossSigningKeyCache!("self", "");
expect(nokey).toBeNull(); expect(nokey).toBeNull();
const key = await getCrossSigningKeyCache("self_signing", ""); const key = await getCrossSigningKeyCache!("self_signing", "");
expect(new Uint8Array(key)).toEqual(testKey); expect(new Uint8Array(key)).toEqual(testKey);
}); });
}); });

View File

@ -90,7 +90,7 @@ const signedDeviceList2: IDownloadKeyResult = {
describe('DeviceList', function() { describe('DeviceList', function() {
let downloadSpy; let downloadSpy;
let cryptoStore; let cryptoStore;
let deviceLists = []; let deviceLists: DeviceList[] = [];
beforeEach(function() { beforeEach(function() {
deviceLists = []; deviceLists = [];

View File

@ -32,8 +32,8 @@ 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';
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')!;
const ROOM_ID = '!ROOM:ID'; const ROOM_ID = '!ROOM:ID';
@ -331,7 +331,7 @@ describe("MegolmDecryption", function() {
}, },
}, },
}); });
mockBaseApis.sendToDevice.mockResolvedValue(undefined); mockBaseApis.sendToDevice.mockResolvedValue({});
mockBaseApis.queueToDevice.mockResolvedValue(undefined); mockBaseApis.queueToDevice.mockResolvedValue(undefined);
aliceDeviceInfo = { aliceDeviceInfo = {
@ -515,8 +515,8 @@ describe("MegolmDecryption", function() {
bobdevice1: { bobdevice1: {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: { keys: {
"ed25519:Dynabook": bobDevice1.deviceEd25519Key, "ed25519:Dynabook": bobDevice1.deviceEd25519Key!,
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key, "curve25519:Dynabook": bobDevice1.deviceCurve25519Key!,
}, },
verified: 0, verified: 0,
known: false, known: false,
@ -524,8 +524,8 @@ describe("MegolmDecryption", function() {
bobdevice2: { bobdevice2: {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: { keys: {
"ed25519:Dynabook": bobDevice2.deviceEd25519Key, "ed25519:Dynabook": bobDevice2.deviceEd25519Key!,
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key, "curve25519:Dynabook": bobDevice2.deviceCurve25519Key!,
}, },
verified: -1, verified: -1,
known: false, known: false,
@ -614,8 +614,8 @@ describe("MegolmDecryption", function() {
bobdevice: { bobdevice: {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: { keys: {
"ed25519:bobdevice": bobDevice.deviceEd25519Key, "ed25519:bobdevice": bobDevice.deviceEd25519Key!,
"curve25519:bobdevice": bobDevice.deviceCurve25519Key, "curve25519:bobdevice": bobDevice.deviceCurve25519Key!,
}, },
verified: 0, verified: 0,
known: true, known: true,
@ -718,8 +718,8 @@ describe("MegolmDecryption", function() {
device_id: "bobdevice", device_id: "bobdevice",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: { keys: {
"ed25519:bobdevice": bobDevice.deviceEd25519Key, "ed25519:bobdevice": bobDevice.deviceEd25519Key!,
"curve25519:bobdevice": bobDevice.deviceCurve25519Key, "curve25519:bobdevice": bobDevice.deviceCurve25519Key!,
}, },
known: true, known: true,
verified: 1, verified: 1,

View File

@ -67,13 +67,13 @@ describe("OlmDevice", function() {
const sid = await setupSession(aliceOlmDevice, bobOlmDevice); const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
const ciphertext = await aliceOlmDevice.encryptMessage( const ciphertext = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key, bobOlmDevice.deviceCurve25519Key!,
sid, sid,
"The olm or proteus is an aquatic salamander in the family Proteidae", "The olm or proteus is an aquatic salamander in the family Proteidae",
) as any; // OlmDevice.encryptMessage has incorrect return type ) as any; // OlmDevice.encryptMessage has incorrect return type
const result = await bobOlmDevice.createInboundSession( const result = await bobOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key, aliceOlmDevice.deviceCurve25519Key!,
ciphertext.type, ciphertext.type,
ciphertext.body, ciphertext.body,
); );
@ -94,7 +94,7 @@ describe("OlmDevice", function() {
+ " in the family Proteidae" + " in the family Proteidae"
); );
const ciphertext = await aliceOlmDevice.encryptMessage( const ciphertext = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key, bobOlmDevice.deviceCurve25519Key!,
sessionId, sessionId,
MESSAGE, MESSAGE,
) as any; // OlmDevice.encryptMessage has incorrect return type ) as any; // OlmDevice.encryptMessage has incorrect return type
@ -103,7 +103,7 @@ describe("OlmDevice", function() {
bobRecreatedOlmDevice.init({ fromExportedDevice: exported }); bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
const decrypted = await bobRecreatedOlmDevice.createInboundSession( const decrypted = await bobRecreatedOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key, aliceOlmDevice.deviceCurve25519Key!,
ciphertext.type, ciphertext.type,
ciphertext.body, ciphertext.body,
); );
@ -118,7 +118,7 @@ describe("OlmDevice", function() {
+ " the olm is entirely aquatic" + " the olm is entirely aquatic"
); );
const ciphertext2 = await aliceOlmDevice.encryptMessage( const ciphertext2 = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key, bobOlmDevice.deviceCurve25519Key!,
sessionId, sessionId,
MESSAGE_2, MESSAGE_2,
) as any; // OlmDevice.encryptMessage has incorrect return type ) as any; // OlmDevice.encryptMessage has incorrect return type
@ -128,7 +128,7 @@ describe("OlmDevice", function() {
// Note: "decrypted_2" does not have the same structure as "decrypted" // Note: "decrypted_2" does not have the same structure as "decrypted"
const decrypted2 = await bobRecreatedAgainOlmDevice.decryptMessage( const decrypted2 = await bobRecreatedAgainOlmDevice.decryptMessage(
aliceOlmDevice.deviceCurve25519Key, aliceOlmDevice.deviceCurve25519Key!,
decrypted.session_id, decrypted.session_id,
ciphertext2.type, ciphertext2.type,
ciphertext2.body, ciphertext2.body,

View File

@ -34,7 +34,7 @@ import { MatrixScheduler } from '../../../src';
const Olm = global.Olm; const Olm = global.Olm;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2'); const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
const ROOM_ID = '!ROOM:ID'; const ROOM_ID = '!ROOM:ID';
@ -197,7 +197,7 @@ describe("MegolmBackup", function() {
// to tick the clock between the first try and the retry. // to tick the clock between the first try and the retry.
const realSetTimeout = global.setTimeout; const realSetTimeout = global.setTimeout;
jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) { jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) {
return realSetTimeout(f!, n/100); return realSetTimeout(f!, n!/100);
}); });
}); });
@ -318,7 +318,7 @@ describe("MegolmBackup", function() {
resolve(); resolve();
return Promise.resolve({} as T); return Promise.resolve({} as T);
}; };
client.crypto.backupManager.backupGroupSession( client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(), groupSession.session_id(),
); );
@ -349,7 +349,7 @@ describe("MegolmBackup", function() {
return client.initCrypto() return client.initCrypto()
.then(() => { .then(() => {
return client.crypto.storeSessionBackupPrivateKey(new Uint8Array(32)); return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
}) })
.then(() => { .then(() => {
return cryptoStore.doTxn( return cryptoStore.doTxn(
@ -401,7 +401,7 @@ describe("MegolmBackup", function() {
resolve(); resolve();
return Promise.resolve({} as T); return Promise.resolve({} as T);
}; };
client.crypto.backupManager.backupGroupSession( client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(), groupSession.session_id(),
); );
@ -449,7 +449,7 @@ describe("MegolmBackup", function() {
try { try {
// make sure auth_data is signed by the master key // make sure auth_data is signed by the master key
olmlib.pkVerify( olmlib.pkVerify(
(data as Record<string, any>).auth_data, client.getCrossSigningId(), "@alice:bar", (data as Record<string, any>).auth_data, client.getCrossSigningId()!, "@alice:bar",
); );
} catch (e) { } catch (e) {
reject(e); reject(e);
@ -568,7 +568,7 @@ describe("MegolmBackup", function() {
); );
} }
}; };
return client.crypto.backupManager.backupGroupSession( return client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(), groupSession.session_id(),
); );
@ -699,4 +699,30 @@ describe("MegolmBackup", function() {
)).rejects.toThrow(); )).rejects.toThrow();
}); });
}); });
describe("flagAllGroupSessionsForBackup", () => {
it("should return number of sesions needing backup", async () => {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
const store = new StubStore();
const client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
fetchFn: jest.fn(), // NOP
store,
scheduler,
userId: "@alice:bar",
deviceId: "device",
cryptoStore,
});
await client.initCrypto();
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);
await expect(client.flagAllGroupSessionsForBackup()).resolves.toBe(6);
client.stopClient();
});
});
}); });

View File

@ -93,8 +93,8 @@ describe("Cross Signing", function() {
); );
alice.uploadDeviceSigningKeys = jest.fn().mockImplementation(async (auth, keys) => { alice.uploadDeviceSigningKeys = jest.fn().mockImplementation(async (auth, keys) => {
await olmlib.verifySignature( await olmlib.verifySignature(
alice.crypto.olmDevice, keys.master_key, "@alice:example.com", alice.crypto!.olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key, "Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!,
); );
}); });
alice.uploadKeySignatures = async () => ({ failures: {} }); alice.uploadKeySignatures = async () => ({ failures: {} });
@ -152,7 +152,7 @@ describe("Cross Signing", function() {
authUploadDeviceSigningKeys, authUploadDeviceSigningKeys,
}); });
} catch (e) { } catch (e) {
if (e.errcode === "M_FORBIDDEN") { if ((<MatrixError>e).errcode === "M_FORBIDDEN") {
bootstrapDidThrow = true; bootstrapDidThrow = true;
} }
} }
@ -169,7 +169,7 @@ describe("Cross Signing", function() {
// set Alice's cross-signing key // set Alice's cross-signing key
await resetCrossSigningKeys(alice); await resetCrossSigningKeys(alice);
// Alice downloads Bob's device key // Alice downloads Bob's device key
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: { keys: {
master: { master: {
user_id: "@bob:example.com", user_id: "@bob:example.com",
@ -238,12 +238,12 @@ describe("Cross Signing", function() {
alice.uploadKeySignatures = jest.fn().mockImplementation(async (content) => { alice.uploadKeySignatures = jest.fn().mockImplementation(async (content) => {
try { try {
await olmlib.verifySignature( await olmlib.verifySignature(
alice.crypto.olmDevice, alice.crypto!.olmDevice,
content["@alice:example.com"][ content["@alice:example.com"][
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
], ],
"@alice:example.com", "@alice:example.com",
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key, "Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!,
); );
olmlib.pkVerify( olmlib.pkVerify(
content["@alice:example.com"]["Osborne2"], content["@alice:example.com"]["Osborne2"],
@ -258,7 +258,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",
@ -266,7 +266,7 @@ describe("Cross Signing", function() {
keys: deviceInfo.keys, keys: deviceInfo.keys,
algorithms: deviceInfo.algorithms, algorithms: deviceInfo.algorithms,
}; };
await alice.crypto.signObject(aliceDevice); await alice.crypto!.signObject(aliceDevice);
olmlib.pkSign( olmlib.pkSign(
aliceDevice as ISignedKey, aliceDevice as ISignedKey,
selfSigningKey as unknown as PkSigning, selfSigningKey as unknown as PkSigning,
@ -401,7 +401,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig, ["ed25519:" + bobMasterPubkey]: sskSig,
}, },
}; };
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: { keys: {
master: { master: {
user_id: "@bob:example.com", user_id: "@bob:example.com",
@ -435,7 +435,7 @@ describe("Cross Signing", function() {
verified: 0, verified: 0,
known: false, known: false,
}; };
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice, Dynabook: bobDevice,
}); });
// Bob's device key should be TOFU // Bob's device key should be TOFU
@ -467,11 +467,11 @@ describe("Cross Signing", function() {
const aliceKeys: Record<string, PkSigning> = {}; const aliceKeys: Record<string, PkSigning> = {};
const { client: alice, httpBackend } = await makeTestClient( const { client: alice, httpBackend } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },
null, undefined,
aliceKeys, aliceKeys,
); );
alice.crypto.deviceList.startTrackingDeviceList("@bob:example.com"); alice.crypto!.deviceList.startTrackingDeviceList("@bob:example.com");
alice.crypto.deviceList.stopTrackingAllDeviceLists = () => {}; alice.crypto!.deviceList.stopTrackingAllDeviceLists = () => {};
alice.uploadDeviceSigningKeys = async () => ({}); alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} }); alice.uploadKeySignatures = async () => ({ failures: {} });
@ -486,7 +486,7 @@ describe("Cross Signing", function() {
]); ]);
const keyChangePromise = new Promise<void>((resolve, reject) => { const keyChangePromise = new Promise<void>((resolve, reject) => {
alice.crypto.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => { alice.crypto!.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => {
if (userId === "@bob:example.com") { if (userId === "@bob:example.com") {
resolve(); resolve();
} }
@ -494,7 +494,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",
@ -502,7 +502,7 @@ describe("Cross Signing", function() {
keys: deviceInfo.keys, keys: deviceInfo.keys,
algorithms: deviceInfo.algorithms, algorithms: deviceInfo.algorithms,
}; };
await alice.crypto.signObject(aliceDevice); await alice.crypto!.signObject(aliceDevice);
const bobOlmAccount = new global.Olm.Account(); const bobOlmAccount = new global.Olm.Account();
bobOlmAccount.create(); bobOlmAccount.create();
@ -667,7 +667,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig, ["ed25519:" + bobMasterPubkey]: sskSig,
}, },
}; };
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: { keys: {
master: { master: {
user_id: "@bob:example.com", user_id: "@bob:example.com",
@ -690,7 +690,7 @@ describe("Cross Signing", function() {
"ed25519:Dynabook": "someOtherPubkey", "ed25519:Dynabook": "someOtherPubkey",
}, },
}; };
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice as unknown as IDevice, Dynabook: bobDevice as unknown as IDevice,
}); });
// Bob's device key should be untrusted // Bob's device key should be untrusted
@ -735,7 +735,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig, ["ed25519:" + bobMasterPubkey]: sskSig,
}, },
}; };
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: { keys: {
master: { master: {
user_id: "@bob:example.com", user_id: "@bob:example.com",
@ -770,7 +770,7 @@ describe("Cross Signing", function() {
}, },
}, },
}; };
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice, Dynabook: bobDevice,
}); });
// Alice verifies Bob's SSK // Alice verifies Bob's SSK
@ -802,7 +802,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey2]: sskSig2, ["ed25519:" + bobMasterPubkey2]: sskSig2,
}, },
}; };
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: { keys: {
master: { master: {
user_id: "@bob:example.com", user_id: "@bob:example.com",
@ -838,8 +838,8 @@ describe("Cross Signing", function() {
// Alice gets new signature for device // Alice gets new signature for device
const sig2 = bobSigning2.sign(bobDeviceString); const sig2 = bobSigning2.sign(bobDeviceString);
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2; bobDevice.signatures!["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice, Dynabook: bobDevice,
}); });
@ -876,20 +876,20 @@ describe("Cross Signing", function() {
bob.uploadKeySignatures = async () => ({ failures: {} }); bob.uploadKeySignatures = async () => ({ failures: {} });
// set Bob's cross-signing key // set Bob's cross-signing key
await resetCrossSigningKeys(bob); await resetCrossSigningKeys(bob);
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: { Dynabook: {
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: { keys: {
"curve25519:Dynabook": bob.crypto.olmDevice.deviceCurve25519Key, "curve25519:Dynabook": bob.crypto!.olmDevice.deviceCurve25519Key!,
"ed25519:Dynabook": bob.crypto.olmDevice.deviceEd25519Key, "ed25519:Dynabook": bob.crypto!.olmDevice.deviceEd25519Key!,
}, },
verified: 1, verified: 1,
known: true, known: true,
}, },
}); });
alice.crypto.deviceList.storeCrossSigningForUser( alice.crypto!.deviceList.storeCrossSigningForUser(
"@bob:example.com", "@bob:example.com",
bob.crypto.crossSigningInfo.toStorage(), bob.crypto!.crossSigningInfo.toStorage(),
); );
alice.uploadDeviceSigningKeys = async () => ({}); alice.uploadDeviceSigningKeys = async () => ({});
@ -909,8 +909,8 @@ describe("Cross Signing", function() {
expect(bobTrust.isTofu()).toBeTruthy(); expect(bobTrust.isTofu()).toBeTruthy();
// "forget" that Bob is trusted // "forget" that Bob is trusted
delete alice.crypto.deviceList.crossSigningInfo["@bob:example.com"] delete alice.crypto!.deviceList.crossSigningInfo["@bob:example.com"]
.keys.master.signatures["@alice:example.com"]; .keys.master.signatures!["@alice:example.com"];
const bobTrust2 = alice.checkUserTrust("@bob:example.com"); const bobTrust2 = alice.checkUserTrust("@bob:example.com");
expect(bobTrust2.isCrossSigningVerified()).toBeFalsy(); expect(bobTrust2.isCrossSigningVerified()).toBeFalsy();
@ -919,9 +919,9 @@ describe("Cross Signing", function() {
upgradePromise = new Promise((resolve) => { upgradePromise = new Promise((resolve) => {
upgradeResolveFunc = resolve; upgradeResolveFunc = resolve;
}); });
alice.crypto.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com"); alice.crypto!.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
await new Promise((resolve) => { await new Promise((resolve) => {
alice.crypto.on(CryptoEvent.UserTrustStatusChanged, resolve); alice.crypto!.on(CryptoEvent.UserTrustStatusChanged, resolve);
}); });
await upgradePromise; await upgradePromise;
@ -963,7 +963,7 @@ describe("Cross Signing", function() {
}; };
// Alice's device downloads the keys, but doesn't trust them yet // Alice's device downloads the keys, but doesn't trust them yet
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", { alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: { keys: {
master: { master: {
user_id: "@alice:example.com", user_id: "@alice:example.com",
@ -999,7 +999,7 @@ describe("Cross Signing", function() {
["ed25519:" + alicePubkey]: sig, ["ed25519:" + alicePubkey]: sig,
}, },
} }; } };
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", { alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
[aliceDeviceId]: aliceCrossSignedDevice, [aliceDeviceId]: aliceCrossSignedDevice,
}); });
@ -1042,7 +1042,7 @@ describe("Cross Signing", function() {
}; };
// Alice's device downloads the keys // Alice's device downloads the keys
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", { alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: { keys: {
master: { master: {
user_id: "@alice:example.com", user_id: "@alice:example.com",
@ -1067,11 +1067,65 @@ describe("Cross Signing", function() {
"ed25519:Dynabook": "someOtherPubkey", "ed25519:Dynabook": "someOtherPubkey",
}, },
}; };
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", { alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
[deviceId]: aliceNotCrossSignedDevice, [deviceId]: aliceNotCrossSignedDevice,
}); });
expect(alice.checkIfOwnDeviceCrossSigned(deviceId)).toBeFalsy(); expect(alice.checkIfOwnDeviceCrossSigned(deviceId)).toBeFalsy();
alice.stopClient(); alice.stopClient();
}); });
it("checkIfOwnDeviceCrossSigned should sanely handle unknown devices", async () => {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
["ed25519:" + alicePubkey]: alicePubkey,
},
};
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
aliceSSK.signatures = {
"@alice:example.com": {
["ed25519:" + aliceMasterPubkey]: sskSig,
},
};
// Alice's device downloads the keys
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
},
},
self_signing: aliceSSK,
},
firstUse: true,
crossSigningVerifiedBefore: false,
});
expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy();
alice.stopClient();
});
it("checkIfOwnDeviceCrossSigned should sanely handle unknown users", async () => {
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy();
alice.stopClient();
});
}); });

View File

@ -39,7 +39,7 @@ export async function createSecretStorageKey(): Promise<IRecoveryKey> {
decryption.free(); decryption.free();
return { return {
// `pubkey` not used anymore with symmetric 4S // `pubkey` not used anymore with symmetric 4S
keyInfo: { pubkey: storagePublicKey, key: undefined }, keyInfo: { pubkey: storagePublicKey, key: undefined! },
privateKey: storagePrivateKey, privateKey: storagePrivateKey,
}; };
} }

View File

@ -93,7 +93,7 @@ describe.each([
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]); await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
expect(r).not.toBeNull(); expect(r).not.toBeNull();
expect(r).not.toBeUndefined(); expect(r).not.toBeUndefined();
expect(r.state).toEqual(RoomKeyRequestState.Sent); expect(r!.state).toEqual(RoomKeyRequestState.Sent);
expect(requests).toContainEqual(r); expect(requests).toContainEqual(r);
}); });
}); });

View File

@ -21,9 +21,9 @@ import { MatrixEvent } from "../../../src/models/event";
import { TestClient } from '../../TestClient'; import { TestClient } from '../../TestClient';
import { makeTestClients } from './verification/util'; import { makeTestClients } from './verification/util';
import { encryptAES } from "../../../src/crypto/aes"; import { encryptAES } from "../../../src/crypto/aes";
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils"; import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from '../../../src/logger'; import { logger } from '../../../src/logger';
import { ICreateClientOpts } from '../../../src/client'; import { ClientEvent, ICreateClientOpts } 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';
@ -41,7 +41,7 @@ async function makeTestClient(userInfo: { userId: string, deviceId: string}, opt
await client.initCrypto(); await client.initCrypto();
// No need to download keys for these tests // No need to download keys for these tests
jest.spyOn(client.crypto, 'downloadKeys').mockResolvedValue({}); jest.spyOn(client.crypto!, 'downloadKeys').mockResolvedValue({});
return client; return client;
} }
@ -93,11 +93,11 @@ describe("Secrets", function() {
}, },
}, },
); );
alice.crypto.crossSigningInfo.setKeys({ alice.crypto!.crossSigningInfo.setKeys({
master: signingkeyInfo, master: signingkeyInfo,
}); });
const secretStorage = alice.crypto.secretStorage; const secretStorage = alice.crypto!.secretStorage;
jest.spyOn(alice, 'setAccountData').mockImplementation( jest.spyOn(alice, 'setAccountData').mockImplementation(
async function(eventType, contents) { async function(eventType, contents) {
@ -113,7 +113,7 @@ describe("Secrets", function() {
const keyAccountData = { const keyAccountData = {
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES, algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
}; };
await alice.crypto.crossSigningInfo.signObject(keyAccountData, 'master'); await alice.crypto!.crossSigningInfo.signObject(keyAccountData, 'master');
alice.store.storeAccountDataEvents([ alice.store.storeAccountDataEvents([
new MatrixEvent({ new MatrixEvent({
@ -200,7 +200,7 @@ describe("Secrets", function() {
await alice.storeSecret("foo", "bar"); await alice.storeSecret("foo", "bar");
const accountData = alice.getAccountData('foo'); const accountData = alice.getAccountData('foo');
expect(accountData.getContent().encrypted).toBeTruthy(); expect(accountData!.getContent().encrypted).toBeTruthy();
alice.stopClient(); alice.stopClient();
}); });
@ -233,29 +233,29 @@ describe("Secrets", function() {
}, },
); );
const vaxDevice = vax.client.crypto.olmDevice; const vaxDevice = vax.client.crypto!.olmDevice;
const osborne2Device = osborne2.client.crypto.olmDevice; const osborne2Device = osborne2.client.crypto!.olmDevice;
const secretStorage = osborne2.client.crypto.secretStorage; const secretStorage = osborne2.client.crypto!.secretStorage;
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { osborne2.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
"VAX": { "VAX": {
known: false, known: false,
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: { keys: {
"ed25519:VAX": vaxDevice.deviceEd25519Key, "ed25519:VAX": vaxDevice.deviceEd25519Key!,
"curve25519:VAX": vaxDevice.deviceCurve25519Key, "curve25519:VAX": vaxDevice.deviceCurve25519Key!,
}, },
verified: DeviceInfo.DeviceVerification.VERIFIED, verified: DeviceInfo.DeviceVerification.VERIFIED,
}, },
}); });
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { vax.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
"Osborne2": { "Osborne2": {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
verified: 0, verified: 0,
known: false, known: false,
keys: { keys: {
"ed25519:Osborne2": osborne2Device.deviceEd25519Key, "ed25519:Osborne2": osborne2Device.deviceEd25519Key!,
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key, "curve25519:Osborne2": osborne2Device.deviceCurve25519Key!,
}, },
}, },
}); });
@ -264,13 +264,13 @@ describe("Secrets", function() {
const otks = (await osborne2Device.getOneTimeKeys()).curve25519; const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
await osborne2Device.markKeysAsPublished(); await osborne2Device.markKeysAsPublished();
await vax.client.crypto.olmDevice.createOutboundSession( await vax.client.crypto!.olmDevice.createOutboundSession(
osborne2Device.deviceCurve25519Key, osborne2Device.deviceCurve25519Key!,
Object.values(otks)[0], Object.values(otks)[0],
); );
osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com"; osborne2.client.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const request = await secretStorage.request("foo", ["VAX"]); const request = await secretStorage.request("foo", ["VAX"]);
await request.promise; // return value not used await request.promise; // return value not used
@ -328,7 +328,7 @@ describe("Secrets", function() {
this.store.storeAccountDataEvents([ this.store.storeAccountDataEvents([
event, event,
]); ]);
this.emit("accountData", event); this.emit(ClientEvent.AccountData, event);
return {}; return {};
}; };
@ -339,8 +339,8 @@ describe("Secrets", function() {
createSecretStorageKey, createSecretStorageKey,
}); });
const crossSigning = bob.crypto.crossSigningInfo; const crossSigning = bob.crypto!.crossSigningInfo;
const secretStorage = bob.crypto.secretStorage; const secretStorage = bob.crypto!.secretStorage;
expect(crossSigning.getId()).toBeTruthy(); expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)) expect(await crossSigning.isStoredInSecretStorage(secretStorage))
@ -486,7 +486,7 @@ describe("Secrets", function() {
}, },
}), }),
]); ]);
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", { alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
firstUse: false, firstUse: false,
crossSigningVerifiedBefore: false, crossSigningVerifiedBefore: false,
keys: { keys: {
@ -528,16 +528,15 @@ describe("Secrets", function() {
content: data, content: data,
}); });
alice.store.storeAccountDataEvents([event]); alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event); this.emit(ClientEvent.AccountData, event);
return {}; return {};
}; };
await alice.bootstrapSecretStorage({}); await alice.bootstrapSecretStorage({});
expect(alice.getAccountData("m.secret_storage.default_key").getContent()) expect(alice.getAccountData("m.secret_storage.default_key")!.getContent())
.toEqual({ key: "key_id" }); .toEqual({ key: "key_id" });
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id") const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>();
.getContent() as ISecretStorageKeyInfo;
expect(keyInfo.algorithm) expect(keyInfo.algorithm)
.toEqual("m.secret_storage.v1.aes-hmac-sha2"); .toEqual("m.secret_storage.v1.aes-hmac-sha2");
expect(keyInfo.passphrase).toEqual({ expect(keyInfo.passphrase).toEqual({
@ -630,7 +629,7 @@ describe("Secrets", function() {
}, },
}), }),
]); ]);
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", { alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
firstUse: false, firstUse: false,
crossSigningVerifiedBefore: false, crossSigningVerifiedBefore: false,
keys: { keys: {
@ -672,14 +671,13 @@ describe("Secrets", function() {
content: data, content: data,
}); });
alice.store.storeAccountDataEvents([event]); alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event); this.emit(ClientEvent.AccountData, event);
return {}; return {};
}; };
await alice.bootstrapSecretStorage({}); await alice.bootstrapSecretStorage({});
const backupKey = alice.getAccountData("m.megolm_backup.v1") const backupKey = alice.getAccountData("m.megolm_backup.v1")!.getContent();
.getContent();
expect(backupKey.encrypted).toHaveProperty("key_id"); expect(backupKey.encrypted).toHaveProperty("key_id");
expect(await alice.getSecret("m.megolm_backup.v1")) expect(await alice.getSecret("m.megolm_backup.v1"))
.toEqual("ey0GB1kB6jhOWgwiBUMIWg=="); .toEqual("ey0GB1kB6jhOWgwiBUMIWg==");

View File

@ -49,7 +49,7 @@ describe("verification request integration tests with crypto layer", function()
verificationMethods: [verificationMethods.SAS], verificationMethods: [verificationMethods.SAS],
}, },
); );
alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() { alice.client.crypto!.deviceList.getRawStoredDevicesForUser = function() {
return { return {
Dynabook: { Dynabook: {
algorithms: [], algorithms: [],

View File

@ -17,16 +17,17 @@ limitations under the License.
import "../../../olm-loader"; import "../../../olm-loader";
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util'; import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
import { MatrixEvent } from "../../../../src/models/event"; import { MatrixEvent } from "../../../../src/models/event";
import { SAS } from "../../../../src/crypto/verification/SAS"; import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo"; import { DeviceInfo } 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 } from "../../../../src/crypto/verification/Base"; import { VerificationBase as Verification, 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";
import { TestClient } from "../../../TestClient";
const Olm = global.Olm; const Olm = global.Olm;
@ -75,13 +76,13 @@ describe("SAS verification", function() {
}); });
describe("verification", () => { describe("verification", () => {
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: Verification<any, any>;
let bobPromise; let bobPromise: Promise<VerificationBase<any, any>>;
let clearTestClientTimeouts; let clearTestClientTimeouts: () => void;
beforeEach(async () => { beforeEach(async () => {
[[alice, bob], clearTestClientTimeouts] = await makeTestClients( [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
@ -94,8 +95,8 @@ describe("SAS verification", function() {
}, },
); );
const aliceDevice = alice.client.crypto.olmDevice; const aliceDevice = alice.client.crypto!.olmDevice;
const bobDevice = bob.client.crypto.olmDevice; const bobDevice = bob.client.crypto!.olmDevice;
ALICE_DEVICES = { ALICE_DEVICES = {
Osborne2: { Osborne2: {
@ -121,26 +122,26 @@ describe("SAS verification", function() {
}, },
}; };
alice.client.crypto.deviceList.storeDevicesForUser( alice.client.crypto!.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES, "@bob:example.com", BOB_DEVICES,
); );
alice.client.downloadKeys = () => { alice.client.downloadKeys = () => {
return Promise.resolve(); return Promise.resolve({});
}; };
bob.client.crypto.deviceList.storeDevicesForUser( bob.client.crypto!.deviceList.storeDevicesForUser(
"@alice:example.com", ALICE_DEVICES, "@alice:example.com", ALICE_DEVICES,
); );
bob.client.downloadKeys = () => { bob.client.downloadKeys = () => {
return Promise.resolve(); return Promise.resolve({});
}; };
aliceSasEvent = null; aliceSasEvent = null;
bobSasEvent = null; bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => { bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on("crypto.verification.request", request => { bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier.on("show_sas", (e) => { request.verifier!.on("show_sas", (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) {
@ -156,14 +157,14 @@ describe("SAS verification", function() {
} }
} }
}); });
resolve(request.verifier); resolve(request.verifier!);
}); });
}); });
aliceVerifier = alice.client.beginKeyVerification( aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId(), bob.deviceId, verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!,
); );
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) {
@ -195,9 +196,9 @@ describe("SAS verification", function() {
const origSendToDevice = bob.client.sendToDevice.bind(bob.client); const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = function(type, map) { bob.client.sendToDevice = function(type, map) {
if (type === "m.key.verification.accept") { if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()][alice.client.deviceId] macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
.message_authentication_code; .message_authentication_code;
keyAgreement = map[alice.client.getUserId()][alice.client.deviceId] keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!]
.key_agreement_protocol; .key_agreement_protocol;
} }
return origSendToDevice(type, map); return origSendToDevice(type, map);
@ -219,8 +220,8 @@ describe("SAS verification", function() {
await Promise.all([ await Promise.all([
aliceVerifier.verify(), aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()), bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(), alice.httpBackend.flush(undefined),
bob.httpBackend.flush(), bob.httpBackend.flush(undefined),
]); ]);
// make sure that it uses the preferred method // make sure that it uses the preferred method
@ -230,10 +231,10 @@ describe("SAS verification", function() {
// make sure Alice and Bob verified each other // make sure Alice and Bob verified each other
const bobDevice const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice.isVerified()).toBeTruthy(); expect(bobDevice?.isVerified()).toBeTruthy();
const aliceDevice const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice.isVerified()).toBeTruthy(); expect(aliceDevice?.isVerified()).toBeTruthy();
}); });
it("should be able to verify using the old base64", async () => { it("should be able to verify using the old base64", async () => {
@ -248,7 +249,7 @@ describe("SAS verification", function() {
// has, since it is the same object. If this does not // has, since it is the same object. If this does not
// happen, the verification will fail due to a hash // happen, the verification will fail due to a hash
// commitment mismatch. // commitment mismatch.
map[bob.client.getUserId()][bob.client.deviceId] map[bob.client.getUserId()!][bob.client.deviceId!]
.message_authentication_codes = ['hkdf-hmac-sha256']; .message_authentication_codes = ['hkdf-hmac-sha256'];
} }
return aliceOrigSendToDevice(type, map); return aliceOrigSendToDevice(type, map);
@ -256,7 +257,7 @@ describe("SAS verification", function() {
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client); const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => { bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") { if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()][alice.client.deviceId] macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
.message_authentication_code; .message_authentication_code;
} }
return bobOrigSendToDevice(type, map); return bobOrigSendToDevice(type, map);
@ -278,18 +279,18 @@ describe("SAS verification", function() {
await Promise.all([ await Promise.all([
aliceVerifier.verify(), aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()), bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(), alice.httpBackend.flush(undefined),
bob.httpBackend.flush(), bob.httpBackend.flush(undefined),
]); ]);
expect(macMethod).toBe("hkdf-hmac-sha256"); expect(macMethod).toBe("hkdf-hmac-sha256");
const bobDevice const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice.isVerified()).toBeTruthy(); expect(bobDevice!.isVerified()).toBeTruthy();
const aliceDevice const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice.isVerified()).toBeTruthy(); expect(aliceDevice!.isVerified()).toBeTruthy();
}); });
it("should be able to verify using the old MAC", async () => { it("should be able to verify using the old MAC", async () => {
@ -304,7 +305,7 @@ describe("SAS verification", function() {
// has, since it is the same object. If this does not // has, since it is the same object. If this does not
// happen, the verification will fail due to a hash // happen, the verification will fail due to a hash
// commitment mismatch. // commitment mismatch.
map[bob.client.getUserId()][bob.client.deviceId] map[bob.client.getUserId()!][bob.client.deviceId!]
.message_authentication_codes = ['hmac-sha256']; .message_authentication_codes = ['hmac-sha256'];
} }
return aliceOrigSendToDevice(type, map); return aliceOrigSendToDevice(type, map);
@ -312,7 +313,7 @@ describe("SAS verification", function() {
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client); const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => { bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") { if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()][alice.client.deviceId] macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
.message_authentication_code; .message_authentication_code;
} }
return bobOrigSendToDevice(type, map); return bobOrigSendToDevice(type, map);
@ -334,18 +335,18 @@ describe("SAS verification", function() {
await Promise.all([ await Promise.all([
aliceVerifier.verify(), aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()), bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(), alice.httpBackend.flush(undefined),
bob.httpBackend.flush(), bob.httpBackend.flush(undefined),
]); ]);
expect(macMethod).toBe("hmac-sha256"); expect(macMethod).toBe("hmac-sha256");
const bobDevice const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice.isVerified()).toBeTruthy(); expect(bobDevice?.isVerified()).toBeTruthy();
const aliceDevice const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice.isVerified()).toBeTruthy(); expect(aliceDevice?.isVerified()).toBeTruthy();
}); });
it("should verify a cross-signing key", async () => { it("should verify a cross-signing key", async () => {
@ -361,9 +362,11 @@ describe("SAS verification", function() {
await resetCrossSigningKeys(bob.client); await resetCrossSigningKeys(bob.client);
bob.client.crypto.deviceList.storeCrossSigningForUser( bob.client.crypto!.deviceList.storeCrossSigningForUser(
"@alice:example.com", { "@alice:example.com", {
keys: alice.client.crypto.crossSigningInfo.keys, keys: alice.client.crypto!.crossSigningInfo.keys,
crossSigningVerifiedBefore: false,
firstUse: true,
}, },
); );
@ -415,10 +418,10 @@ 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) => { request.verifier!.on("show_sas", (e) => {
e.mismatch(); e.mismatch();
}); });
resolve(request.verifier); resolve(request.verifier!);
}); });
}); });
@ -464,7 +467,7 @@ describe("SAS verification", function() {
}, },
); );
alice.client.crypto.setDeviceVerification = jest.fn(); alice.client.crypto!.setDeviceVerification = jest.fn();
alice.client.getDeviceEd25519Key = () => { alice.client.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key"; return "alice+base64+ed25519+key";
}; };
@ -482,7 +485,7 @@ describe("SAS verification", function() {
return Promise.resolve(); return Promise.resolve();
}; };
bob.client.crypto.setDeviceVerification = jest.fn(); bob.client.crypto!.setDeviceVerification = jest.fn();
bob.client.getStoredDevice = () => { bob.client.getStoredDevice = () => {
return DeviceInfo.fromStorage( return DeviceInfo.fromStorage(
{ {
@ -565,7 +568,7 @@ describe("SAS verification", function() {
]); ]);
// make sure Alice and Bob verified each other // make sure Alice and Bob verified each other
expect(alice.client.crypto.setDeviceVerification) expect(alice.client.crypto!.setDeviceVerification)
.toHaveBeenCalledWith( .toHaveBeenCalledWith(
bob.client.getUserId(), bob.client.getUserId(),
bob.client.deviceId, bob.client.deviceId,
@ -574,7 +577,7 @@ describe("SAS verification", function() {
null, null,
{ "ed25519:Dynabook": "bob+base64+ed25519+key" }, { "ed25519:Dynabook": "bob+base64+ed25519+key" },
); );
expect(bob.client.crypto.setDeviceVerification) expect(bob.client.crypto!.setDeviceVerification)
.toHaveBeenCalledWith( .toHaveBeenCalledWith(
alice.client.getUserId(), alice.client.getUserId(),
alice.client.deviceId, alice.client.deviceId,

View File

@ -41,7 +41,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
}); });
const client = clientMap[userId][deviceId]; const client = clientMap[userId][deviceId];
const decryptionPromise = event.isEncrypted() ? const decryptionPromise = event.isEncrypted() ?
event.attemptDecryption(client.crypto) : event.attemptDecryption(client.crypto!) :
Promise.resolve(); Promise.resolve();
decryptionPromise.then( decryptionPromise.then(

View File

@ -32,7 +32,7 @@ describe("eventMapperFor", function() {
fetchFn: function() {} as any, // NOP fetchFn: function() {} as any, // NOP
store: { store: {
getRoom(roomId: string): Room | null { getRoom(roomId: string): Room | null {
return rooms.find(r => r.roomId === roomId); return rooms.find(r => r.roomId === roomId) ?? null;
}, },
} as IStore, } as IStore,
scheduler: { scheduler: {

View File

@ -50,8 +50,8 @@ describe('EventTimelineSet', () => {
EventType.RoomMessage, EventType.RoomMessage,
); );
expect(relations).toBeDefined(); expect(relations).toBeDefined();
expect(relations.getRelations().length).toBe(1); expect(relations!.getRelations().length).toBe(1);
expect(relations.getRelations()[0].getId()).toBe(replyEvent.getId()); expect(relations!.getRelations()[0].getId()).toBe(replyEvent.getId());
}); });
}; };

View File

@ -21,7 +21,7 @@ describe("EventTimeline", function() {
const getTimeline = (): EventTimeline => { const getTimeline = (): EventTimeline => {
const room = new Room(roomId, mockClient, userA); const room = new Room(roomId, mockClient, userA);
const timelineSet = new EventTimelineSet(room); const timelineSet = new EventTimelineSet(room);
jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet); jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
return new EventTimeline(timelineSet); return new EventTimeline(timelineSet);
}; };

View File

@ -1,3 +1,19 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync"; import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
import { Filter, IFilterDefinition } from "../../src/filter"; import { Filter, IFilterDefinition } from "../../src/filter";
import { mkEvent } from "../test-utils/test-utils"; import { mkEvent } from "../test-utils/test-utils";

View File

@ -36,7 +36,7 @@ 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, Room } from "../../src"; import { ContentHelpers, EventTimeline, MatrixError, 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 {
@ -88,21 +88,22 @@ describe("MatrixClient", function() {
data: SYNC_DATA, data: SYNC_DATA,
}; };
let httpLookups = [ // items are popped off when processed and block if no items left.
// items are objects which look like: let httpLookups: {
// { method: string;
// method: "GET", path: string;
// path: "/initialSync", data?: object;
// data: {}, error?: object;
// error: { errcode: M_FORBIDDEN } // if present will reject promise, expectBody?: object;
// expectBody: {} // additional expects on the body expectQueryParams?: object;
// expectQueryParams: {} // additional expects on query params thenCall?: Function;
// thenCall: function(){} // function to call *AFTER* returning response. }[] = [];
// }
// items are popped off when processed and block if no items left.
];
let acceptKeepalives: boolean; let acceptKeepalives: boolean;
let pendingLookup = null; let pendingLookup: {
promise: Promise<any>;
method: string;
path: string;
} | null = null;
function httpReq(method, path, qp, data, prefix) { function httpReq(method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH && acceptKeepalives) { if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
return Promise.resolve({ return Promise.resolve({
@ -144,7 +145,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]);
}); });
} }
@ -155,9 +156,9 @@ describe("MatrixClient", function() {
if (next.error) { if (next.error) {
// eslint-disable-next-line // eslint-disable-next-line
return Promise.reject({ return Promise.reject({
errcode: next.error.errcode, errcode: (<MatrixError>next.error).errcode,
httpStatus: next.error.httpStatus, httpStatus: (<MatrixError>next.error).httpStatus,
name: next.error.errcode, name: (<MatrixError>next.error).errcode,
message: "Expected testing error", message: "Expected testing error",
data: next.error, data: next.error,
}); });
@ -254,7 +255,7 @@ describe("MatrixClient", function() {
type: UNSTABLE_MSC3088_PURPOSE.unstable, type: UNSTABLE_MSC3088_PURPOSE.unstable,
state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.unstable, state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.unstable,
content: { content: {
[UNSTABLE_MSC3088_ENABLED.unstable]: true, [UNSTABLE_MSC3088_ENABLED.unstable!]: true,
}, },
}, },
{ {
@ -299,7 +300,7 @@ describe("MatrixClient", function() {
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable); expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
return new MatrixEvent({ return new MatrixEvent({
content: { content: {
[UNSTABLE_MSC3088_ENABLED.unstable]: true, [UNSTABLE_MSC3088_ENABLED.unstable!]: true,
}, },
}); });
} else { } else {
@ -359,7 +360,7 @@ describe("MatrixClient", function() {
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable); expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
return new MatrixEvent({ return new MatrixEvent({
content: { content: {
[UNSTABLE_MSC3088_ENABLED.unstable]: true, [UNSTABLE_MSC3088_ENABLED.unstable!]: true,
}, },
}); });
} else { } else {
@ -393,7 +394,7 @@ describe("MatrixClient", function() {
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable); expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
return new MatrixEvent({ return new MatrixEvent({
content: { content: {
[UNSTABLE_MSC3088_ENABLED.unstable]: false, [UNSTABLE_MSC3088_ENABLED.unstable!]: false,
}, },
}); });
} else { } else {
@ -599,14 +600,14 @@ 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 = []; const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]); expectedStates.push(["PREPARED", null]);
client.on("sync", syncChecker(expectedStates, done)); client.on("sync", syncChecker(expectedStates, done));
client.startClient(); client.startClient();
}); });
it("should transition null -> ERROR after a failed /filter", function(done) { it("should transition null -> ERROR after a failed /filter", function(done) {
const expectedStates = []; const expectedStates: [string, string | null][] = [];
httpLookups = []; httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE); httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({ httpLookups.push({
@ -620,36 +621,35 @@ describe("MatrixClient", function() {
// Disabled because now `startClient` makes a legit call to `/versions` // Disabled because now `startClient` makes a legit call to `/versions`
// And those tests are really unhappy about it... Not possible to figure // And those tests are really unhappy about it... Not possible to figure
// out what a good resolution would look like // out what a good resolution would look like
xit("should transition ERROR -> CATCHUP after /sync if prev failed", xit("should transition ERROR -> CATCHUP after /sync if prev failed", function(done) {
function(done) { const expectedStates: [string, string | null][] = [];
const expectedStates = []; acceptKeepalives = false;
acceptKeepalives = false; httpLookups = [];
httpLookups = []; httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE); httpLookups.push(FILTER_RESPONSE);
httpLookups.push(FILTER_RESPONSE); httpLookups.push({
httpLookups.push({ method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }, });
}); httpLookups.push({
httpLookups.push({ method: "GET", path: KEEP_ALIVE_PATH,
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" },
error: { errcode: "KEEPALIVE_FAIL" }, });
}); httpLookups.push({
httpLookups.push({ method: "GET", path: KEEP_ALIVE_PATH, data: {},
method: "GET", path: KEEP_ALIVE_PATH, data: {}, });
}); httpLookups.push({
httpLookups.push({ method: "GET", path: "/sync", data: SYNC_DATA,
method: "GET", path: "/sync", data: SYNC_DATA,
});
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
}); });
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition PREPARED -> SYNCING after /sync", function(done) { it("should transition PREPARED -> SYNCING after /sync", function(done) {
const expectedStates = []; 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("sync", syncChecker(expectedStates, done));
@ -658,7 +658,7 @@ describe("MatrixClient", function() {
xit("should transition SYNCING -> ERROR after a failed /sync", function(done) { xit("should transition SYNCING -> ERROR after a failed /sync", function(done) {
acceptKeepalives = false; acceptKeepalives = false;
const expectedStates = []; const expectedStates: [string, string | null][] = [];
httpLookups.push({ httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }, method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
}); });
@ -675,37 +675,35 @@ describe("MatrixClient", function() {
client.startClient(); client.startClient();
}); });
xit("should transition ERROR -> SYNCING after /sync if prev failed", xit("should transition ERROR -> SYNCING after /sync if prev failed", function(done) {
function(done) { const expectedStates: [string, string | null][] = [];
const expectedStates = []; httpLookups.push({
httpLookups.push({ method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
}); });
httpLookups.push(SYNC_RESPONSE);
it("should transition SYNCING -> SYNCING on subsequent /sync successes", expectedStates.push(["PREPARED", null]);
function(done) { expectedStates.push(["SYNCING", "PREPARED"]);
const expectedStates = []; expectedStates.push(["ERROR", "SYNCING"]);
httpLookups.push(SYNC_RESPONSE); client.on("sync", syncChecker(expectedStates, done));
httpLookups.push(SYNC_RESPONSE); client.startClient();
});
expectedStates.push(["PREPARED", null]); it("should transition SYNCING -> SYNCING on subsequent /sync successes", function(done) {
expectedStates.push(["SYNCING", "PREPARED"]); const expectedStates: [string, string | null][] = [];
expectedStates.push(["SYNCING", "SYNCING"]); httpLookups.push(SYNC_RESPONSE);
client.on("sync", syncChecker(expectedStates, done)); httpLookups.push(SYNC_RESPONSE);
client.startClient();
}); expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
xit("should transition ERROR -> ERROR if keepalive keeps failing", function(done) { xit("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
acceptKeepalives = false; acceptKeepalives = false;
const expectedStates = []; const expectedStates: [string, string | null][] = [];
httpLookups.push({ httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }, method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
}); });

View File

@ -209,32 +209,32 @@ describe('NotificationService', function() {
msgtype: "m.text", msgtype: "m.text",
}, },
}); });
matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules); matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules!);
pushProcessor = new PushProcessor(matrixClient); pushProcessor = new PushProcessor(matrixClient);
}); });
// User IDs // User IDs
it('should bing on a user ID.', function() { it('should bing on a user ID.', function() {
testEvent.event.content.body = "Hello @ali:matrix.org, how are you?"; testEvent.event.content!.body = "Hello @ali:matrix.org, how are you?";
const actions = pushProcessor.actionsForEvent(testEvent); const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
}); });
it('should bing on a partial user ID with an @.', function() { it('should bing on a partial user ID with an @.', function() {
testEvent.event.content.body = "Hello @ali, how are you?"; testEvent.event.content!.body = "Hello @ali, how are you?";
const actions = pushProcessor.actionsForEvent(testEvent); const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
}); });
it('should bing on a partial user ID without @.', function() { it('should bing on a partial user ID without @.', function() {
testEvent.event.content.body = "Hello ali, how are you?"; testEvent.event.content!.body = "Hello ali, how are you?";
const actions = pushProcessor.actionsForEvent(testEvent); const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
}); });
it('should bing on a case-insensitive user ID.', function() { it('should bing on a case-insensitive user ID.', function() {
testEvent.event.content.body = "Hello @AlI:matrix.org, how are you?"; testEvent.event.content!.body = "Hello @AlI:matrix.org, how are you?";
const actions = pushProcessor.actionsForEvent(testEvent); const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
}); });
@ -242,13 +242,13 @@ describe('NotificationService', function() {
// Display names // Display names
it('should bing on a display name.', function() { it('should bing on a display name.', function() {
testEvent.event.content.body = "Hello Alice M, how are you?"; testEvent.event.content!.body = "Hello Alice M, how are you?";
const actions = pushProcessor.actionsForEvent(testEvent); const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
}); });
it('should bing on a case-insensitive display name.', function() { it('should bing on a case-insensitive display name.', function() {
testEvent.event.content.body = "Hello ALICE M, how are you?"; testEvent.event.content!.body = "Hello ALICE M, how are you?";
const actions = pushProcessor.actionsForEvent(testEvent); const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
}); });
@ -256,43 +256,43 @@ describe('NotificationService', function() {
// Bing words // Bing words
it('should bing on a bing word.', function() { it('should bing on a bing word.', function() {
testEvent.event.content.body = "I really like coffee"; testEvent.event.content!.body = "I really like coffee";
const actions = pushProcessor.actionsForEvent(testEvent); const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
}); });
it('should bing on case-insensitive bing words.', function() { it('should bing on case-insensitive bing words.', function() {
testEvent.event.content.body = "Coffee is great"; testEvent.event.content!.body = "Coffee is great";
const actions = pushProcessor.actionsForEvent(testEvent); const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
}); });
it('should bing on wildcard (.*) bing words.', function() { it('should bing on wildcard (.*) bing words.', function() {
testEvent.event.content.body = "It was foomahbar I think."; testEvent.event.content!.body = "It was foomahbar I think.";
const actions = pushProcessor.actionsForEvent(testEvent); const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
}); });
it('should bing on character group ([abc]) bing words.', function() { it('should bing on character group ([abc]) bing words.', function() {
testEvent.event.content.body = "Ping!"; testEvent.event.content!.body = "Ping!";
let actions = pushProcessor.actionsForEvent(testEvent); let actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "Pong!"; testEvent.event.content!.body = "Pong!";
actions = pushProcessor.actionsForEvent(testEvent); actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
}); });
it('should bing on character range ([a-z]) bing words.', function() { it('should bing on character range ([a-z]) bing words.', function() {
testEvent.event.content.body = "I ate 6 pies"; testEvent.event.content!.body = "I ate 6 pies";
const actions = pushProcessor.actionsForEvent(testEvent); const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
}); });
it('should bing on character negation ([!a]) bing words.', function() { it('should bing on character negation ([!a]) bing words.', function() {
testEvent.event.content.body = "boke"; testEvent.event.content!.body = "boke";
let actions = pushProcessor.actionsForEvent(testEvent); let actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true); expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "bake"; testEvent.event.content!.body = "bake";
actions = pushProcessor.actionsForEvent(testEvent); actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false); expect(actions.tweaks.highlight).toEqual(false);
}); });
@ -316,7 +316,7 @@ describe('NotificationService', function() {
// invalid // invalid
it('should gracefully handle bad input.', function() { it('should gracefully handle bad input.', function() {
testEvent.event.content.body = { "foo": "bar" }; testEvent.event.content!.body = { "foo": "bar" };
const actions = pushProcessor.actionsForEvent(testEvent); const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false); expect(actions.tweaks.highlight).toEqual(false);
}); });

View File

@ -18,6 +18,7 @@ import { EventTimelineSet } from "../../src/models/event-timeline-set";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event"; import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { Room } from "../../src/models/room"; import { Room } from "../../src/models/room";
import { Relations } from "../../src/models/relations"; import { Relations } from "../../src/models/relations";
import { TestClient } from "../TestClient";
describe("Relations", function() { describe("Relations", function() {
it("should deduplicate annotations", function() { it("should deduplicate annotations", function() {
@ -43,7 +44,7 @@ describe("Relations", function() {
// Add the event once and check results // Add the event once and check results
{ {
relations.addEvent(eventA); relations.addEvent(eventA);
const annotationsByKey = relations.getSortedAnnotationsByKey(); const annotationsByKey = relations.getSortedAnnotationsByKey()!;
expect(annotationsByKey.length).toEqual(1); expect(annotationsByKey.length).toEqual(1);
const [key, events] = annotationsByKey[0]; const [key, events] = annotationsByKey[0];
expect(key).toEqual("👍️"); expect(key).toEqual("👍️");
@ -53,7 +54,7 @@ describe("Relations", function() {
// Add the event again and expect the same // Add the event again and expect the same
{ {
relations.addEvent(eventA); relations.addEvent(eventA);
const annotationsByKey = relations.getSortedAnnotationsByKey(); const annotationsByKey = relations.getSortedAnnotationsByKey()!;
expect(annotationsByKey.length).toEqual(1); expect(annotationsByKey.length).toEqual(1);
const [key, events] = annotationsByKey[0]; const [key, events] = annotationsByKey[0];
expect(key).toEqual("👍️"); expect(key).toEqual("👍️");
@ -66,7 +67,7 @@ describe("Relations", function() {
// Add the event again and expect the same // Add the event again and expect the same
{ {
relations.addEvent(eventB); relations.addEvent(eventB);
const annotationsByKey = relations.getSortedAnnotationsByKey(); const annotationsByKey = relations.getSortedAnnotationsByKey()!;
expect(annotationsByKey.length).toEqual(1); expect(annotationsByKey.length).toEqual(1);
const [key, events] = annotationsByKey[0]; const [key, events] = annotationsByKey[0];
expect(key).toEqual("👍️"); expect(key).toEqual("👍️");
@ -179,4 +180,28 @@ describe("Relations", function() {
expect(badlyEditedTopic.replacingEvent()).toBe(null); expect(badlyEditedTopic.replacingEvent()).toBe(null);
expect(badlyEditedTopic.getContent().topic).toBe("topic"); expect(badlyEditedTopic.getContent().topic).toBe("topic");
}); });
it("getSortedAnnotationsByKey should return null for non-annotation relations", async () => {
const userId = "@user:server";
const room = new Room("room123", new TestClient(userId).client, userId);
const relations = new Relations("m.replace", "m.room.message", room);
// Create an instance of an annotation
const eventData = {
"sender": "@bob:example.com",
"type": "m.room.message",
"event_id": "$cZ1biX33ENJqIm00ks0W_hgiO_6CHrsAc3ZQrnLeNTw",
"room_id": "!pzVjCQSoQPpXQeHpmK:example.com",
"content": {
"m.relates_to": {
"event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
"rel_type": "m.replace",
},
},
};
const eventA = new MatrixEvent(eventData);
relations.addEvent(eventA);
expect(relations.getSortedAnnotationsByKey()).toBeNull();
});
}); });

View File

@ -601,7 +601,7 @@ describe("Room", function() {
}); });
const resetTimelineTests = function(timelineSupport) { const resetTimelineTests = function(timelineSupport) {
let events = null; let events: MatrixEvent[];
beforeEach(function() { beforeEach(function() {
room = new Room(roomId, new TestClient(userA).client, userA, { timelineSupport: timelineSupport }); room = new Room(roomId, new TestClient(userA).client, userA, { timelineSupport: timelineSupport });
@ -1732,7 +1732,7 @@ describe("Room", function() {
client.members.mockReturnValue({ chunk: [memberEvent] }); client.members.mockReturnValue({ chunk: [memberEvent] });
await room.loadMembersIfNeeded(); await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar"); const memberA = room.getMember("@user_a:bar")!;
expect(memberA.name).toEqual("User A"); expect(memberA.name).toEqual("User A");
}); });
}); });
@ -2455,28 +2455,28 @@ describe("Room", function() {
room.addLiveEvents(events); room.addLiveEvents(events);
const thread = threadRoot.getThread(); const thread = threadRoot.getThread()!;
expect(thread.rootEvent).toBe(threadRoot); expect(thread.rootEvent).toBe(threadRoot);
const rootRelations = thread.timelineSet.relations.getChildEventsForEvent( const rootRelations = thread.timelineSet.relations.getChildEventsForEvent(
threadRoot.getId(), threadRoot.getId(),
RelationType.Annotation, RelationType.Annotation,
EventType.Reaction, EventType.Reaction,
).getSortedAnnotationsByKey(); )!.getSortedAnnotationsByKey();
expect(rootRelations).toHaveLength(1); expect(rootRelations).toHaveLength(1);
expect(rootRelations[0][0]).toEqual(rootReaction.getRelation().key); expect(rootRelations![0][0]).toEqual(rootReaction.getRelation()!.key);
expect(rootRelations[0][1].size).toEqual(1); expect(rootRelations![0][1].size).toEqual(1);
expect(rootRelations[0][1].has(rootReaction)).toBeTruthy(); expect(rootRelations![0][1].has(rootReaction)).toBeTruthy();
const responseRelations = thread.timelineSet.relations.getChildEventsForEvent( const responseRelations = thread.timelineSet.relations.getChildEventsForEvent(
threadResponse.getId(), threadResponse.getId(),
RelationType.Annotation, RelationType.Annotation,
EventType.Reaction, EventType.Reaction,
).getSortedAnnotationsByKey(); )!.getSortedAnnotationsByKey();
expect(responseRelations).toHaveLength(1); expect(responseRelations).toHaveLength(1);
expect(responseRelations[0][0]).toEqual(threadReaction.getRelation().key); expect(responseRelations![0][0]).toEqual(threadReaction.getRelation()!.key);
expect(responseRelations[0][1].size).toEqual(1); expect(responseRelations![0][1].size).toEqual(1);
expect(responseRelations[0][1].has(threadReaction)).toBeTruthy(); expect(responseRelations![0][1].has(threadReaction)).toBeTruthy();
}); });
}); });

View File

@ -37,7 +37,7 @@ const mockClient = {
function createTimeline(numEvents = 3, baseIndex = 1): EventTimeline { function createTimeline(numEvents = 3, baseIndex = 1): EventTimeline {
const room = new Room(ROOM_ID, mockClient, USER_ID); const room = new Room(ROOM_ID, mockClient, USER_ID);
const timelineSet = new EventTimelineSet(room); const timelineSet = new EventTimelineSet(room);
jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet); jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
const timeline = new EventTimeline(timelineSet); const timeline = new EventTimeline(timelineSet);
@ -170,7 +170,7 @@ describe("TimelineWindow", function() {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
mockClient.getEventTimeline.mockResolvedValue(undefined); mockClient.getEventTimeline.mockResolvedValue(undefined);
mockClient.paginateEventTimeline.mockReturnValue(undefined); mockClient.paginateEventTimeline.mockResolvedValue(false);
}); });
describe("load", function() { describe("load", function() {

View File

@ -26,17 +26,19 @@ import {
MockRTCPeerConnection, MockRTCPeerConnection,
} from "../../test-utils/webrtc"; } from "../../test-utils/webrtc";
import { CallFeed } from "../../../src/webrtc/callFeed"; import { CallFeed } from "../../../src/webrtc/callFeed";
import { EventType, MatrixClient } from "../../../src";
import { MediaHandler } from "../../../src/webrtc/mediaHandler";
const startVoiceCall = async (client: TestClient, call: MatrixCall): Promise<void> => { const startVoiceCall = async (client: TestClient, call: MatrixCall): Promise<void> => {
const callPromise = call.placeVoiceCall(); const callPromise = call.placeVoiceCall();
await client.httpBackend.flush(""); await client.httpBackend!.flush("");
await callPromise; await callPromise;
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" }); call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
}; };
describe('Call', function() { describe('Call', function() {
let client; let client: TestClient;
let call; let call;
let prevNavigator; let prevNavigator;
let prevDocument; let prevDocument;
@ -71,10 +73,10 @@ describe('Call', function() {
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {}); client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
// We just stub out sendEvent: we're not interested in testing the client's // We just stub out sendEvent: we're not interested in testing the client's
// event sending code here // event sending code here
client.client.sendEvent = () => {}; client.client.sendEvent = (() => {}) as unknown as MatrixClient["sendEvent"];
client.client.mediaHandler = new MockMediaHandler; client.client["mediaHandler"] = new MockMediaHandler as unknown as MediaHandler;
client.client.getMediaHandler = () => client.client.mediaHandler; client.client.getMediaHandler = () => client.client["mediaHandler"]!;
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {}); client.httpBackend!.when("GET", "/voip/turnServer").respond(200, {});
call = new MatrixCall({ call = new MatrixCall({
client: client.client, client: client.client,
roomId: '!foo:bar', roomId: '!foo:bar',
@ -237,7 +239,7 @@ describe('Call', function() {
expect(identChangedCallback).toHaveBeenCalled(); expect(identChangedCallback).toHaveBeenCalled();
const ident = call.getRemoteAssertedIdentity(); const ident = call.getRemoteAssertedIdentity()!;
expect(ident.id).toEqual("@steve:example.com"); expect(ident.id).toEqual("@steve:example.com");
expect(ident.displayName).toEqual("Steve Gibbons"); expect(ident.displayName).toEqual("Steve Gibbons");
@ -306,19 +308,19 @@ describe('Call', function() {
}); });
it("should fallback to answering with no video", async () => { it("should fallback to answering with no video", async () => {
await client.httpBackend.flush(); await client.httpBackend!.flush(undefined);
call.shouldAnswerWithMediaType = (wantedValue: boolean) => wantedValue; call.shouldAnswerWithMediaType = (wantedValue: boolean) => wantedValue;
client.client.mediaHandler.getUserMediaStream = jest.fn().mockRejectedValue("reject"); client.client["mediaHandler"].getUserMediaStream = jest.fn().mockRejectedValue("reject");
await call.answer(true, true); await call.answer(true, true);
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(1, true, true); expect(client.client["mediaHandler"].getUserMediaStream).toHaveBeenNthCalledWith(1, true, true);
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(2, true, false); expect(client.client["mediaHandler"].getUserMediaStream).toHaveBeenNthCalledWith(2, true, false);
}); });
it("should handle mid-call device changes", async () => { it("should handle mid-call device changes", async () => {
client.client.mediaHandler.getUserMediaStream = jest.fn().mockReturnValue( client.client["mediaHandler"].getUserMediaStream = jest.fn().mockReturnValue(
new MockMediaStream( new MockMediaStream(
"stream", [ "stream", [
new MockMediaStreamTrack("audio_track", "audio"), new MockMediaStreamTrack("audio_track", "audio"),
@ -424,7 +426,7 @@ describe('Call', function() {
it("should choose opponent member", async () => { it("should choose opponent member", async () => {
const callPromise = call.placeVoiceCall(); const callPromise = call.placeVoiceCall();
await client.httpBackend.flush(); await client.httpBackend!.flush(undefined);
await callPromise; await callPromise;
const opponentMember = { const opponentMember = {
@ -480,7 +482,7 @@ describe('Call', function() {
it("should correctly generate local SDPStreamMetadata", async () => { it("should correctly generate local SDPStreamMetadata", async () => {
const callPromise = call.placeCallWithCallFeeds([new CallFeed({ const callPromise = call.placeCallWithCallFeeds([new CallFeed({
client, client: client.client,
// @ts-ignore Mock // @ts-ignore Mock
stream: new MockMediaStream("local_stream1", [new MockMediaStreamTrack("track_id", "audio")]), stream: new MockMediaStream("local_stream1", [new MockMediaStreamTrack("track_id", "audio")]),
roomId: call.roomId, roomId: call.roomId,
@ -489,7 +491,7 @@ describe('Call', function() {
audioMuted: false, audioMuted: false,
videoMuted: false, videoMuted: false,
})]); })]);
await client.httpBackend.flush(); await client.httpBackend!.flush(undefined);
await callPromise; await callPromise;
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" }); call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
@ -521,7 +523,7 @@ describe('Call', function() {
const callPromise = call.placeCallWithCallFeeds([ const callPromise = call.placeCallWithCallFeeds([
new CallFeed({ new CallFeed({
client, client: client.client,
userId: client.getUserId(), userId: client.getUserId(),
// @ts-ignore Mock // @ts-ignore Mock
stream: localUsermediaStream, stream: localUsermediaStream,
@ -531,7 +533,7 @@ describe('Call', function() {
videoMuted: false, videoMuted: false,
}), }),
new CallFeed({ new CallFeed({
client, client: client.client,
userId: client.getUserId(), userId: client.getUserId(),
// @ts-ignore Mock // @ts-ignore Mock
stream: localScreensharingStream, stream: localScreensharingStream,
@ -541,7 +543,7 @@ describe('Call', function() {
videoMuted: false, videoMuted: false,
}), }),
]); ]);
await client.httpBackend.flush(); await client.httpBackend!.flush(undefined);
await callPromise; await callPromise;
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" }); call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
@ -586,14 +588,14 @@ describe('Call', function() {
getLocalAge: () => null, getLocalAge: () => null,
}); });
call.feeds.push(new CallFeed({ call.feeds.push(new CallFeed({
client, client: client.client,
userId: "remote_user_id", userId: "remote_user_id",
// @ts-ignore Mock // @ts-ignore Mock
stream: new MockMediaStream("remote_stream_id", [new MockMediaStreamTrack("remote_tack_id")]), stream: new MockMediaStream("remote_stream_id", [new MockMediaStreamTrack("remote_tack_id")]),
id: "remote_feed_id", id: "remote_feed_id",
purpose: SDPStreamMetadataPurpose.Usermedia, purpose: SDPStreamMetadataPurpose.Usermedia,
})); }));
await client.httpBackend.flush(); await client.httpBackend!.flush(undefined);
await callPromise; await callPromise;
const callHangupCallback = jest.fn(); const callHangupCallback = jest.fn();
@ -664,10 +666,10 @@ describe('Call', function() {
}); });
it("should return false if window or document are undefined", () => { it("should return false if window or document are undefined", () => {
global.window = undefined; global.window = undefined!;
expect(supportsMatrixCall()).toBe(false); expect(supportsMatrixCall()).toBe(false);
global.window = prevWindow; global.window = prevWindow;
global.document = undefined; global.document = undefined!;
expect(supportsMatrixCall()).toBe(false); expect(supportsMatrixCall()).toBe(false);
}); });
@ -685,9 +687,9 @@ describe('Call', function() {
it("should return false if RTCPeerConnection & RTCSessionDescription " + it("should return false if RTCPeerConnection & RTCSessionDescription " +
"& RTCIceCandidate & mediaDevices are unavailable", "& RTCIceCandidate & mediaDevices are unavailable",
() => { () => {
global.window.RTCPeerConnection = undefined; global.window.RTCPeerConnection = undefined!;
global.window.RTCSessionDescription = undefined; global.window.RTCSessionDescription = undefined!;
global.window.RTCIceCandidate = undefined; global.window.RTCIceCandidate = undefined!;
// @ts-ignore - writing to a read-only property as we are simulating faulty browsers // @ts-ignore - writing to a read-only property as we are simulating faulty browsers
global.navigator.mediaDevices = undefined; global.navigator.mediaDevices = undefined;
expect(supportsMatrixCall()).toBe(false); expect(supportsMatrixCall()).toBe(false);
@ -752,4 +754,17 @@ describe('Call', function() {
expect(call.pushLocalFeed).toHaveBeenCalled(); expect(call.pushLocalFeed).toHaveBeenCalled();
}); });
}); });
describe("transferToCall", () => {
it("should send the required events", async () => {
const targetCall = new MatrixCall({ client: client.client });
const sendEvent = jest.spyOn(client.client, "sendEvent");
await call.transferToCall(targetCall);
const newCallId = (sendEvent.mock.calls[0][2] as any)!.await_call;
expect(sendEvent).toHaveBeenCalledWith(call.roomId, EventType.CallReplaces, expect.objectContaining({
create_call: newCallId,
}));
});
});
}); });

View File

@ -58,7 +58,7 @@ describe("callEventHandler", () => {
client.on(CallEventHandlerEvent.Incoming, incomingCallEmitted); client.on(CallEventHandlerEvent.Incoming, incomingCallEmitted);
client.getSyncState = jest.fn().mockReturnValue(SyncState.Syncing); client.getSyncState = jest.fn().mockReturnValue(SyncState.Syncing);
client.emit(ClientEvent.Sync, SyncState.Syncing); client.emit(ClientEvent.Sync, SyncState.Syncing, null);
expect(incomingCallEmitted).not.toHaveBeenCalled(); expect(incomingCallEmitted).not.toHaveBeenCalled();
}); });

View File

@ -23,7 +23,10 @@ import { Optional } from "matrix-events-sdk/lib/types";
export class NamespacedValue<S extends string, U extends string> { export class NamespacedValue<S extends string, U extends string> {
// Stable is optional, but one of the two parameters is required, hence the weird-looking types. // Stable is optional, but one of the two parameters is required, hence the weird-looking types.
// Goal is to to have developers explicitly say there is no stable value (if applicable). // Goal is to to have developers explicitly say there is no stable value (if applicable).
public constructor(public readonly stable: S | null | undefined, public readonly unstable?: U) { public constructor(stable: S, unstable: U);
public constructor(stable: S, unstable?: U);
public constructor(stable: null | undefined, unstable: U);
public constructor(public readonly stable?: S | null, public readonly unstable?: U) {
if (!this.unstable && !this.stable) { if (!this.unstable && !this.stable) {
throw new Error("One of stable or unstable values must be supplied"); throw new Error("One of stable or unstable values must be supplied");
} }
@ -33,10 +36,10 @@ export class NamespacedValue<S extends string, U extends string> {
if (this.stable) { if (this.stable) {
return this.stable; return this.stable;
} }
return this.unstable; return this.unstable!;
} }
public get altName(): U | S | null { public get altName(): U | S | null | undefined {
if (!this.stable) { if (!this.stable) {
return null; return null;
} }
@ -57,7 +60,7 @@ export class NamespacedValue<S extends string, U extends string> {
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class // this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace. // so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
public findIn<T>(obj: any): Optional<T> { public findIn<T>(obj: any): Optional<T> {
let val: T; let val: T | undefined = undefined;
if (this.name) { if (this.name) {
val = obj?.[this.name]; val = obj?.[this.name];
} }
@ -91,7 +94,7 @@ export class ServerControlledNamespacedValue<S extends string, U extends string>
if (this.stable && !this.preferUnstable) { if (this.stable && !this.preferUnstable) {
return this.stable; return this.stable;
} }
return this.unstable; return this.unstable!;
} }
} }
@ -109,10 +112,10 @@ export class UnstableValue<S extends string, U extends string> extends Namespace
} }
public get name(): U { public get name(): U {
return this.unstable; return this.unstable!;
} }
public get altName(): S { public get altName(): S {
return this.stable; return this.stable!;
} }
} }

View File

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { logger } from "./logger"; import { logger } from "./logger";
import { MatrixClient } from "./matrix"; import { MatrixError, MatrixClient } from "./matrix";
import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage"; import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage";
import { MatrixScheduler } from "./scheduler"; import { MatrixScheduler } from "./scheduler";
@ -72,7 +72,7 @@ export class ToDeviceMessageQueue {
logger.debug("Attempting to send queued to-device messages"); logger.debug("Attempting to send queued to-device messages");
this.sending = true; this.sending = true;
let headBatch: IndexedToDeviceBatch; let headBatch: IndexedToDeviceBatch | null;
try { try {
while (this.running) { while (this.running) {
headBatch = await this.client.store.getOldestToDeviceBatch(); headBatch = await this.client.store.getOldestToDeviceBatch();
@ -90,11 +90,11 @@ export class ToDeviceMessageQueue {
++this.retryAttempts; ++this.retryAttempts;
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const retryDelay = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(null, this.retryAttempts, e); const retryDelay = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(null, this.retryAttempts, <MatrixError>e);
if (retryDelay === -1) { if (retryDelay === -1) {
// the scheduler function doesn't differentiate between fatal errors and just getting // the scheduler function doesn't differentiate between fatal errors and just getting
// bored and giving up for now // bored and giving up for now
if (Math.floor(e.httpStatus / 100) === 4) { if (Math.floor((<MatrixError>e).httpStatus! / 100) === 4) {
logger.error("Fatal error when sending to-device message - dropping to-device batch!", e); logger.error("Fatal error when sending to-device message - dropping to-device batch!", e);
await this.client.store.removeToDeviceBatch(headBatch!.id); await this.client.store.removeToDeviceBatch(headBatch!.id);
} else { } else {

View File

@ -347,7 +347,7 @@ export class AutoDiscovery {
* @returns {Promise<object>} Resolves to the domain's client config. Can * @returns {Promise<object>} Resolves to the domain's client config. Can
* be an empty object. * be an empty object.
*/ */
public static async getRawClientConfig(domain: string): Promise<IClientWellKnown> { public static async getRawClientConfig(domain?: string): Promise<IClientWellKnown> {
if (!domain || typeof(domain) !== "string" || domain.length === 0) { if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length"); throw new Error("'domain' must be a string of non-zero length");
} }

View File

@ -434,7 +434,7 @@ export interface IStartClientOpts {
} }
export interface IStoredClientOpts extends IStartClientOpts { export interface IStoredClientOpts extends IStartClientOpts {
crypto: Crypto; crypto?: Crypto;
canResetEntireTimeline: ResetTimelineCallback; canResetEntireTimeline: ResetTimelineCallback;
} }
@ -697,41 +697,31 @@ export interface IMyDevice {
[UNSTABLE_MSC3852_LAST_SEEN_UA.unstable]?: string; [UNSTABLE_MSC3852_LAST_SEEN_UA.unstable]?: string;
} }
export interface Keys {
keys: { [keyId: string]: string };
usage: string[];
user_id: string;
}
export interface SigningKeys extends Keys {
signatures: ISignatures;
}
export interface DeviceKeys {
[deviceId: string]: IDeviceKeys & {
unsigned?: {
device_display_name: string;
};
};
}
export interface IDownloadKeyResult { export interface IDownloadKeyResult {
failures: { [serverName: string]: object }; failures: { [serverName: string]: object };
device_keys: { device_keys: { [userId: string]: DeviceKeys };
[userId: string]: {
[deviceId: string]: IDeviceKeys & {
unsigned?: {
device_display_name: string;
};
};
};
};
// the following three fields were added in 1.1 // the following three fields were added in 1.1
master_keys?: { master_keys?: { [userId: string]: Keys };
[userId: string]: { self_signing_keys?: { [userId: string]: SigningKeys };
keys: { [keyId: string]: string }; user_signing_keys?: { [userId: string]: SigningKeys };
usage: string[];
user_id: string;
};
};
self_signing_keys?: {
[userId: string]: {
keys: { [keyId: string]: string };
signatures: ISignatures;
usage: string[];
user_id: string;
};
};
user_signing_keys?: {
[userId: string]: {
keys: { [keyId: string]: string };
signatures: ISignatures;
usage: string[];
user_id: string;
};
};
} }
export interface IClaimOTKsResult { export interface IClaimOTKsResult {
@ -886,7 +876,7 @@ export type EmittedEvents = ClientEvent
| BeaconEvent; | BeaconEvent;
export type ClientEventHandlerMap = { export type ClientEventHandlerMap = {
[ClientEvent.Sync]: (state: SyncState, lastState?: SyncState, data?: ISyncStateData) => void; [ClientEvent.Sync]: (state: SyncState, lastState: SyncState | null, data?: ISyncStateData) => void;
[ClientEvent.Event]: (event: MatrixEvent) => void; [ClientEvent.Event]: (event: MatrixEvent) => void;
[ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void; [ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void;
[ClientEvent.AccountData]: (event: MatrixEvent, lastEvent?: MatrixEvent) => void; [ClientEvent.AccountData]: (event: MatrixEvent, lastEvent?: MatrixEvent) => void;
@ -943,22 +933,22 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// We don't technically support this usage, but have reasons to do this. // We don't technically support this usage, but have reasons to do this.
protected canSupportVoip = false; protected canSupportVoip = false;
protected peekSync: SyncApi = null; protected peekSync: SyncApi | null = null;
protected isGuestAccount = false; protected isGuestAccount = false;
protected ongoingScrollbacks: {[roomId: string]: {promise?: Promise<Room>, errorTs?: number}} = {}; protected ongoingScrollbacks: {[roomId: string]: {promise?: Promise<Room>, errorTs?: number}} = {};
protected notifTimelineSet: EventTimelineSet = null; protected notifTimelineSet: EventTimelineSet | null = null;
protected cryptoStore: CryptoStore; protected cryptoStore: CryptoStore;
protected verificationMethods: VerificationMethod[]; protected verificationMethods: VerificationMethod[];
protected fallbackICEServerAllowed = false; protected fallbackICEServerAllowed = false;
protected roomList: RoomList; protected roomList: RoomList;
protected syncApi: SlidingSyncSdk | SyncApi; protected syncApi?: SlidingSyncSdk | SyncApi;
public roomNameGenerator?: ICreateClientOpts["roomNameGenerator"]; public roomNameGenerator?: ICreateClientOpts["roomNameGenerator"];
public pushRules: IPushRules; public pushRules?: IPushRules;
protected syncLeftRoomsPromise: Promise<Room[]>; protected syncLeftRoomsPromise?: Promise<Room[]>;
protected syncedLeftRooms = false; protected syncedLeftRooms = false;
protected clientOpts: IStoredClientOpts; protected clientOpts?: IStoredClientOpts;
protected clientWellKnownIntervalID: ReturnType<typeof setInterval>; protected clientWellKnownIntervalID?: ReturnType<typeof setInterval>;
protected canResetTimelineCallback: ResetTimelineCallback; protected canResetTimelineCallback?: ResetTimelineCallback;
public canSupport = new Map<Feature, ServerSupport>(); public canSupport = new Map<Feature, ServerSupport>();
@ -967,7 +957,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// Promise to a response of the server's /versions response // Promise to a response of the server's /versions response
// TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020 // TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020
protected serverVersionsPromise: Promise<IServerVersions>; protected serverVersionsPromise?: Promise<IServerVersions>;
public cachedCapabilities: { public cachedCapabilities: {
capabilities: ICapabilities; capabilities: ICapabilities;
@ -1242,7 +1232,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.clientRunning = false; this.clientRunning = false;
this.syncApi?.stop(); this.syncApi?.stop();
this.syncApi = null; this.syncApi = undefined;
this.peekSync?.stopPeeking(); this.peekSync?.stopPeeking();
@ -1268,7 +1258,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* to the new device ID if the dehydration was successful. * to the new device ID if the dehydration was successful.
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public async rehydrateDevice(): Promise<string> { public async rehydrateDevice(): Promise<string | undefined> {
if (this.crypto) { if (this.crypto) {
throw new Error("Cannot rehydrate device after crypto is initialized"); throw new Error("Cannot rehydrate device after crypto is initialized");
} }
@ -1317,7 +1307,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}, },
); );
if (rehydrateResult.success === true) { if (rehydrateResult.success) {
this.deviceId = getDeviceResult.device_id; this.deviceId = getDeviceResult.device_id;
logger.info("using dehydrated device"); logger.info("using dehydrated device");
const pickleKey = this.pickleKey || "DEFAULT_KEY"; const pickleKey = this.pickleKey || "DEFAULT_KEY";
@ -1395,7 +1385,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
key: Uint8Array, key: Uint8Array,
keyInfo: IDehydratedDeviceKeyInfo, keyInfo: IDehydratedDeviceKeyInfo,
deviceDisplayName?: string, deviceDisplayName?: string,
): Promise<string> { ): Promise<string | undefined> {
if (!this.crypto) { if (!this.crypto) {
logger.warn('not dehydrating device if crypto is not enabled'); logger.warn('not dehydrating device if crypto is not enabled');
return; return;
@ -1404,7 +1394,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.crypto.dehydrationManager.dehydrateDevice(); return this.crypto.dehydrationManager.dehydrateDevice();
} }
public async exportDevice(): Promise<IExportedDevice> { public async exportDevice(): Promise<IExportedDevice | undefined> {
if (!this.crypto) { if (!this.crypto) {
logger.warn('not exporting device if crypto is not enabled'); logger.warn('not exporting device if crypto is not enabled');
return; return;
@ -1452,7 +1442,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Get the domain for this client's MXID * Get the domain for this client's MXID
* @return {?string} Domain of this MXID * @return {?string} Domain of this MXID
*/ */
public getDomain(): string { public getDomain(): string | null {
if (this.credentials && this.credentials.userId) { if (this.credentials && this.credentials.userId) {
return this.credentials.userId.replace(/^.*?:/, ''); return this.credentials.userId.replace(/^.*?:/, '');
} }
@ -1527,11 +1517,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {?SyncState} the sync state, which may be null. * @return {?SyncState} the sync state, which may be null.
* @see module:client~MatrixClient#event:"sync" * @see module:client~MatrixClient#event:"sync"
*/ */
public getSyncState(): SyncState { public getSyncState(): SyncState | null {
if (!this.syncApi) { return this.syncApi?.getSyncState() ?? null;
return null;
}
return this.syncApi.getSyncState();
} }
/** /**
@ -1600,7 +1587,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public retryImmediately(): boolean { public retryImmediately(): boolean {
// don't await for this promise: we just want to kick it off // don't await for this promise: we just want to kick it off
this.toDeviceMessageQueue.sendQueue(); this.toDeviceMessageQueue.sendQueue();
return this.syncApi.retryImmediately(); return this.syncApi?.retryImmediately() ?? false;
} }
/** /**
@ -1608,7 +1595,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* *
* @return {EventTimelineSet} the globl notification EventTimelineSet * @return {EventTimelineSet} the globl notification EventTimelineSet
*/ */
public getNotifTimelineSet(): EventTimelineSet { public getNotifTimelineSet(): EventTimelineSet | null {
return this.notifTimelineSet; return this.notifTimelineSet;
} }
@ -1759,9 +1746,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {?string} base64-encoded ed25519 key. Null if crypto is * @return {?string} base64-encoded ed25519 key. Null if crypto is
* disabled. * disabled.
*/ */
public getDeviceEd25519Key(): string { public getDeviceEd25519Key(): string | null {
if (!this.crypto) return null; return this.crypto?.getDeviceEd25519Key() ?? null;
return this.crypto.getDeviceEd25519Key();
} }
/** /**
@ -1770,9 +1756,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {?string} base64-encoded curve25519 key. Null if crypto is * @return {?string} base64-encoded curve25519 key. Null if crypto is
* disabled. * disabled.
*/ */
public getDeviceCurve25519Key(): string { public getDeviceCurve25519Key(): string | null {
if (!this.crypto) return null; return this.crypto?.getDeviceCurve25519Key() ?? null;
return this.crypto.getDeviceCurve25519Key();
} }
/** /**
@ -1900,9 +1885,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
private async setDeviceVerification( private async setDeviceVerification(
userId: string, userId: string,
deviceId: string, deviceId: string,
verified: boolean, verified?: boolean | null,
blocked: boolean, blocked?: boolean | null,
known: boolean, known?: boolean | null,
): Promise<void> { ): Promise<void> {
if (!this.crypto) { if (!this.crypto) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
@ -2058,7 +2043,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* *
* @returns {string} the key ID * @returns {string} the key ID
*/ */
public getCrossSigningId(type: CrossSigningKey | string = CrossSigningKey.Master): string { public getCrossSigningId(type: CrossSigningKey | string = CrossSigningKey.Master): string | null {
if (!this.crypto) { if (!this.crypto) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
} }
@ -2074,7 +2059,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* *
* @returns {CrossSigningInfo} the cross signing information for the user. * @returns {CrossSigningInfo} the cross signing information for the user.
*/ */
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo { public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
if (!this.crypto) { if (!this.crypto) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
} }
@ -2408,7 +2393,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* *
* @return {string} the contents of the secret * @return {string} the contents of the secret
*/ */
public getSecret(name: string): Promise<string> { public getSecret(name: string): Promise<string | undefined> {
if (!this.crypto) { if (!this.crypto) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
} }
@ -2656,7 +2641,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* trust information (as returned by isKeyBackupTrusted) * trust information (as returned by isKeyBackupTrusted)
* in trustInfo. * in trustInfo.
*/ */
public checkKeyBackup(): Promise<IKeyBackupCheck> { public checkKeyBackup(): Promise<IKeyBackupCheck | null> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.backupManager.checkKeyBackup(); return this.crypto.backupManager.checkKeyBackup();
} }
@ -2672,7 +2660,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
{ prefix: ClientPrefix.V3 }, { prefix: ClientPrefix.V3 },
); );
} catch (e) { } catch (e) {
if (e.errcode === 'M_NOT_FOUND') { if ((<MatrixError>e).errcode === 'M_NOT_FOUND') {
return null; return null;
} else { } else {
throw e; throw e;
@ -2693,6 +2681,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* } * }
*/ */
public isKeyBackupTrusted(info: IKeyBackupInfo): Promise<TrustInfo> { public isKeyBackupTrusted(info: IKeyBackupInfo): Promise<TrustInfo> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.backupManager.isKeyBackupTrusted(info); return this.crypto.backupManager.isKeyBackupTrusted(info);
} }
@ -2701,7 +2692,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* the server, otherwise false. If we haven't completed a successful check * the server, otherwise false. If we haven't completed a successful check
* of key backup status yet, returns null. * of key backup status yet, returns null.
*/ */
public getKeyBackupEnabled(): boolean { public getKeyBackupEnabled(): boolean | null {
if (!this.crypto) { if (!this.crypto) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
} }
@ -2750,7 +2741,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
// TODO: Verify types // TODO: Verify types
public async prepareKeyBackupVersion( public async prepareKeyBackupVersion(
password?: string, password?: string | Uint8Array | null,
opts: IKeyBackupPrepareOpts = { secureSecretStorage: false }, opts: IKeyBackupPrepareOpts = { secureSecretStorage: false },
): Promise<Pick<IPreparedKeyBackupVersion, "algorithm" | "auth_data" | "recovery_key">> { ): Promise<Pick<IPreparedKeyBackupVersion, "algorithm" | "auth_data" | "recovery_key">> {
if (!this.crypto) { if (!this.crypto) {
@ -3053,17 +3044,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
targetSessionId?: string, targetSessionId?: string,
opts?: IKeyBackupRestoreOpts, opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult> { ): Promise<IKeyBackupRestoreResult> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
const storedKey = await this.getSecret("m.megolm_backup.v1"); const storedKey = await this.getSecret("m.megolm_backup.v1");
// ensure that the key is in the right format. If not, fix the key and // ensure that the key is in the right format. If not, fix the key and
// store the fixed version // store the fixed version
const fixedKey = fixBackupKey(storedKey); const fixedKey = fixBackupKey(storedKey);
if (fixedKey) { if (fixedKey) {
const [keyId] = await this.crypto.getSecretStorageKey(); const keys = await this.crypto.getSecretStorageKey();
await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]); await this.storeSecret("m.megolm_backup.v1", fixedKey, [keys![0]]);
} }
const privKey = decodeBase64(fixedKey || storedKey); const privKey = decodeBase64(fixedKey || storedKey!);
return this.restoreKeyBackup( return this.restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo, opts, privKey, targetRoomId, targetSessionId, backupInfo, opts,
); );
@ -3406,7 +3400,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
public setAccountData(eventType: EventType | string, content: IContent): Promise<{}> { public setAccountData(eventType: EventType | string, content: IContent): Promise<{}> {
const path = utils.encodeUri("/user/$userId/account_data/$type", { const path = utils.encodeUri("/user/$userId/account_data/$type", {
$userId: this.credentials.userId, $userId: this.credentials.userId!,
$type: eventType, $type: eventType,
}); });
return retryNetworkOperation(5, () => { return retryNetworkOperation(5, () => {
@ -3442,7 +3436,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return event.getContent<T>(); return event.getContent<T>();
} }
const path = utils.encodeUri("/user/$userId/account_data/$type", { const path = utils.encodeUri("/user/$userId/account_data/$type", {
$userId: this.credentials.userId, $userId: this.credentials.userId!,
$type: eventType, $type: eventType,
}); });
try { try {
@ -3506,7 +3500,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
const room = this.getRoom(roomIdOrAlias); const room = this.getRoom(roomIdOrAlias);
if (room?.hasMembershipState(this.credentials.userId, "join")) { if (room?.hasMembershipState(this.credentials.userId!, "join")) {
return Promise.resolve(room); return Promise.resolve(room);
} }
@ -3621,7 +3615,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
public getRoomTags(roomId: string): Promise<ITagsResponse> { public getRoomTags(roomId: string): Promise<ITagsResponse> {
const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags", { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags", {
$userId: this.credentials.userId, $userId: this.credentials.userId!,
$roomId: roomId, $roomId: roomId,
}); });
return this.http.authedRequest(Method.Get, path); return this.http.authedRequest(Method.Get, path);
@ -3636,7 +3630,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
public setRoomTag(roomId: string, tagName: string, metadata: ITagMetadata): Promise<{}> { public setRoomTag(roomId: string, tagName: string, metadata: ITagMetadata): Promise<{}> {
const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
$userId: this.credentials.userId, $userId: this.credentials.userId!,
$roomId: roomId, $roomId: roomId,
$tag: tagName, $tag: tagName,
}); });
@ -3651,7 +3645,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
public deleteRoomTag(roomId: string, tagName: string): Promise<{}> { public deleteRoomTag(roomId: string, tagName: string): Promise<{}> {
const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
$userId: this.credentials.userId, $userId: this.credentials.userId!,
$roomId: roomId, $roomId: roomId,
$tag: tagName, $tag: tagName,
}); });
@ -3671,7 +3665,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
content: Record<string, any>, content: Record<string, any>,
): Promise<{}> { ): Promise<{}> {
const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", { const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", {
$userId: this.credentials.userId, $userId: this.credentials.userId!,
$roomId: roomId, $roomId: roomId,
$type: eventType, $type: eventType,
}); });
@ -3734,8 +3728,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
roomId: string, roomId: string,
beaconInfoContent: MBeaconInfoEventContent, beaconInfoContent: MBeaconInfoEventContent,
) { ) {
const userId = this.getUserId(); return this.sendStateEvent(roomId, M_BEACON_INFO.name, beaconInfoContent, this.getUserId()!);
return this.sendStateEvent(roomId, M_BEACON_INFO.name, beaconInfoContent, userId);
} }
/** /**
@ -3827,7 +3820,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
})); }));
const room = this.getRoom(roomId); const room = this.getRoom(roomId);
const thread = room?.getThread(threadId); const thread = threadId ? room?.getThread(threadId) : undefined;
if (thread) { if (thread) {
localEvent.setThread(thread); localEvent.setThread(thread);
} }
@ -3847,8 +3840,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// this event does get sent, we have the correct event_id // this event does get sent, we have the correct event_id
const targetId = localEvent.getAssociatedId(); const targetId = localEvent.getAssociatedId();
if (targetId?.startsWith("~")) { if (targetId?.startsWith("~")) {
const target = room.getPendingEvents().find(e => e.getId() === targetId); const target = room?.getPendingEvents().find(e => e.getId() === targetId);
target.once(MatrixEventEvent.LocalEventIdReplaced, () => { target?.once(MatrixEventEvent.LocalEventIdReplaced, () => {
localEvent.updateAssociatedId(target.getId()); localEvent.updateAssociatedId(target.getId());
}); });
} }
@ -3879,13 +3872,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns {Promise} returns a promise which resolves with the result of the send request * @returns {Promise} returns a promise which resolves with the result of the send request
* @private * @private
*/ */
private encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse> { private encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse> {
let cancelled = false; let cancelled = false;
// Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections, // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections,
// so that we can handle synchronous and asynchronous exceptions with the // so that we can handle synchronous and asynchronous exceptions with the
// same code path. // same code path.
return Promise.resolve().then(() => { return Promise.resolve().then(() => {
const encryptionPromise = this.encryptEventIfNeeded(event, room); const encryptionPromise = this.encryptEventIfNeeded(event, room ?? undefined);
if (!encryptionPromise) return null; // doesn't need encryption if (!encryptionPromise) return null; // doesn't need encryption
this.pendingEventEncryption.set(event.getId(), encryptionPromise); this.pendingEventEncryption.set(event.getId(), encryptionPromise);
@ -3900,14 +3893,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}); });
}).then(() => { }).then(() => {
if (cancelled) return {} as ISendEventResponse; if (cancelled) return {} as ISendEventResponse;
let promise: Promise<ISendEventResponse>; let promise: Promise<ISendEventResponse> | null = null;
if (this.scheduler) { if (this.scheduler) {
// if this returns a promise then the scheduler has control now and will // if this returns a promise then the scheduler has control now and will
// resolve/reject when it is done. Internally, the scheduler will invoke // resolve/reject when it is done. Internally, the scheduler will invoke
// processFn which is set to this._sendEventHttpRequest so the same code // processFn which is set to this._sendEventHttpRequest so the same code
// path is executed regardless. // path is executed regardless.
promise = this.scheduler.queueEvent(event); promise = this.scheduler.queueEvent(event);
if (promise && this.scheduler.getQueueForEvent(event).length > 1) { if (promise && this.scheduler.getQueueForEvent(event)!.length > 1) {
// event is processed FIFO so if the length is 2 or more we know // event is processed FIFO so if the length is 2 or more we know
// this event is stuck behind an earlier event. // this event is stuck behind an earlier event.
this.updatePendingEventStatus(room, event, EventStatus.QUEUED); this.updatePendingEventStatus(room, event, EventStatus.QUEUED);
@ -3933,11 +3926,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// consistent at that point. // consistent at that point.
event.error = err; event.error = err;
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT); this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
// also put the event object on the error: the caller will need this
// to resend or cancel the event
err.event = event;
} catch (e) { } catch (e) {
logger.error("Exception in error handler!", e.stack || err); logger.error("Exception in error handler!", (<Error>e).stack || err);
}
if (err instanceof MatrixError) {
err.event = event;
} }
throw err; throw err;
}); });
@ -4128,8 +4121,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// reasonably large pool of messages to parse. // reasonably large pool of messages to parse.
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> => { const makeContentExtensible = (content: IContent = {}, recurse = true): IPartialEvent<object> | undefined => {
let newEvent: IPartialEvent<object> = null; let newEvent: IPartialEvent<object> | 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();
@ -4818,7 +4811,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
const populationResults: { [roomId: string]: Error } = {}; const populationResults: { [roomId: string]: Error } = {};
const promises = []; const promises: Promise<any>[] = [];
const doLeave = (roomId: string) => { const doLeave = (roomId: string) => {
return this.leave(roomId).then(() => { return this.leave(roomId).then(() => {
@ -4907,7 +4900,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
private membershipChange( private membershipChange(
roomId: string, roomId: string,
userId: string, userId: string | undefined,
membership: string, membership: string,
reason?: string, reason?: string,
): Promise<{}> { // API returns an empty object ): Promise<{}> { // API returns an empty object
@ -5020,13 +5013,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
public async setPresence(opts: IPresenceOpts): Promise<void> { public async setPresence(opts: IPresenceOpts): Promise<void> {
const path = utils.encodeUri("/presence/$userId/status", { const path = utils.encodeUri("/presence/$userId/status", {
$userId: this.credentials.userId, $userId: this.credentials.userId!,
}); });
if (typeof opts === "string") {
opts = { presence: opts }; // legacy
}
const validStates = ["offline", "online", "unavailable"]; const validStates = ["offline", "online", "unavailable"];
if (validStates.indexOf(opts.presence) === -1) { if (validStates.indexOf(opts.presence) === -1) {
throw new Error("Bad presence value: " + opts.presence); throw new Error("Bad presence value: " + opts.presence);
@ -5086,7 +5075,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// reduce the required number of events appropriately // reduce the required number of events appropriately
limit = limit - numAdded; limit = limit - numAdded;
const prom = new Promise<Room>((resolve, reject) => { const promise = new Promise<Room>((resolve, reject) => {
// wait for a time before doing this request // wait for a time before doing this request
// (which may be 0 in order not to special case the code paths) // (which may be 0 in order not to special case the code paths)
sleep(timeToWaitMs).then(() => { sleep(timeToWaitMs).then(() => {
@ -5114,7 +5103,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.oldState.paginationToken = null; room.oldState.paginationToken = null;
} }
this.store.storeEvents(room, matrixEvents, res.end, true); this.store.storeEvents(room, matrixEvents, res.end, true);
this.ongoingScrollbacks[room.roomId] = null; delete this.ongoingScrollbacks[room.roomId];
resolve(room); resolve(room);
}).catch((err) => { }).catch((err) => {
this.ongoingScrollbacks[room.roomId] = { this.ongoingScrollbacks[room.roomId] = {
@ -5124,13 +5113,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}); });
}); });
info = { info = { promise };
promise: prom,
errorTs: null,
};
this.ongoingScrollbacks[room.roomId] = info; this.ongoingScrollbacks[room.roomId] = info;
return prom; return promise;
} }
/** /**
@ -5181,7 +5167,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
); );
let params: Record<string, string | string[]> | undefined = undefined; let params: Record<string, string | string[]> | undefined = undefined;
if (this.clientOpts.lazyLoadMembers) { if (this.clientOpts?.lazyLoadMembers) {
params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) }; params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) };
} }
@ -5343,7 +5329,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
let filter: IRoomEventFilter | null = null; let filter: IRoomEventFilter | null = null;
if (this.clientOpts.lazyLoadMembers) { if (this.clientOpts?.lazyLoadMembers) {
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
// so the timelineFilter doesn't get written into it below // so the timelineFilter doesn't get written into it below
filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER); filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER);
@ -5393,7 +5379,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
let filter: IRoomEventFilter | null = null; let filter: IRoomEventFilter | null = null;
if (this.clientOpts.lazyLoadMembers) { if (this.clientOpts?.lazyLoadMembers) {
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
// so the timelineFilter doesn't get written into it below // so the timelineFilter doesn't get written into it below
filter = { filter = {
@ -5441,7 +5427,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> { public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> {
const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet); const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet);
const room = this.getRoom(eventTimeline.getRoomId()); const room = this.getRoom(eventTimeline.getRoomId()!);
const isThreadTimeline = eventTimeline.getTimelineSet().isThreadTimeline; const isThreadTimeline = eventTimeline.getTimelineSet().isThreadTimeline;
// TODO: we should implement a backoff (as per scrollback()) to deal more // TODO: we should implement a backoff (as per scrollback()) to deal more
@ -5519,7 +5505,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
promise = this.createThreadListMessagesRequest( promise = this.createThreadListMessagesRequest(
eventTimeline.getRoomId(), eventTimeline.getRoomId()!,
token, token,
opts.limit, opts.limit,
dir, dir,
@ -5555,7 +5541,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
promise = this.createMessagesRequest( promise = this.createMessagesRequest(
eventTimeline.getRoomId(), eventTimeline.getRoomId()!,
token, token,
opts.limit, opts.limit,
dir, dir,
@ -5612,7 +5598,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// know about /notifications, so we have no choice but to start paginating // know about /notifications, so we have no choice but to start paginating
// from the current point in time. This may well overlap with historical // from the current point in time. This may well overlap with historical
// notifs which are then inserted into the timeline by /sync responses. // notifs which are then inserted into the timeline by /sync responses.
this.notifTimelineSet.resetLiveTimeline('end', null); this.notifTimelineSet.resetLiveTimeline('end');
// we could try to paginate a single event at this point in order to get // we could try to paginate a single event at this point in order to get
// a more valid pagination token, but it just ends up with an out of order // a more valid pagination token, but it just ends up with an out of order
@ -5634,9 +5620,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 peekInRoom(roomId: string): Promise<Room> { public peekInRoom(roomId: string): Promise<Room> {
if (this.peekSync) { this.peekSync?.stopPeeking();
this.peekSync.stopPeeking();
}
this.peekSync = new SyncApi(this, this.clientOpts); this.peekSync = new SyncApi(this, this.clientOpts);
return this.peekSync.peek(roomId); return this.peekSync.peek(roomId);
} }
@ -5943,8 +5927,8 @@ 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> | void { public setRoomMutePushRule(scope: string, roomId: string, mute: boolean): Promise<void> | undefined {
let promise: Promise<unknown>; let promise: Promise<unknown> | undefined;
let hasDontNotifyRule = false; let hasDontNotifyRule = false;
// Get the existing room-kind push rule if any // Get the existing room-kind push rule if any
@ -5986,7 +5970,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (promise) { if (promise) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
// Update this.pushRules when the operation completes // Update this.pushRules when the operation completes
promise.then(() => { promise!.then(() => {
this.getPushRules().then((result) => { this.getPushRules().then((result) => {
this.pushRules = result; this.pushRules = result;
resolve(); resolve();
@ -6173,7 +6157,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
logger.log("Marking success of sync left room request"); logger.log("Marking success of sync left room request");
this.syncedLeftRooms = true; // flip the bit on success this.syncedLeftRooms = true; // flip the bit on success
}).finally(() => { }).finally(() => {
this.syncLeftRoomsPromise = null; // cleanup ongoing request state this.syncLeftRoomsPromise = undefined; // cleanup ongoing request state
}); });
return this.syncLeftRoomsPromise; return this.syncLeftRoomsPromise;
@ -6235,13 +6219,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
public async getOrCreateFilter(filterName: string, filter: Filter): Promise<string> { public async getOrCreateFilter(filterName: string, filter: Filter): Promise<string> {
const filterId = this.store.getFilterIdByName(filterName); const filterId = this.store.getFilterIdByName(filterName);
let existingId = undefined; let existingId: string | undefined;
if (filterId) { if (filterId) {
// check that the existing filter matches our expectations // check that the existing filter matches our expectations
try { try {
const existingFilter = const existingFilter = await this.getFilter(this.credentials.userId!, filterId, true);
await this.getFilter(this.credentials.userId, filterId, true);
if (existingFilter) { if (existingFilter) {
const oldDef = existingFilter.getDefinition(); const oldDef = existingFilter.getDefinition();
const newDef = filter.getDefinition(); const newDef = filter.getDefinition();
@ -6260,7 +6243,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// name: "M_UNKNOWN", // name: "M_UNKNOWN",
// message: "No row found", // message: "No row found",
// } // }
if (error.errcode !== "M_UNKNOWN" && error.errcode !== "M_NOT_FOUND") { if ((<MatrixError>error).errcode !== "M_UNKNOWN" && (<MatrixError>error).errcode !== "M_NOT_FOUND") {
throw error; throw error;
} }
} }
@ -6413,7 +6396,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public isSynapseAdministrator(): Promise<boolean> { public isSynapseAdministrator(): Promise<boolean> {
const path = utils.encodeUri( const path = utils.encodeUri(
"/_synapse/admin/v1/users/$userId/admin", "/_synapse/admin/v1/users/$userId/admin",
{ $userId: this.getUserId() }, { $userId: this.getUserId()! },
); );
return this.http.authedRequest( return this.http.authedRequest(
Method.Get, path, undefined, undefined, { prefix: '' }, Method.Get, path, undefined, undefined, { prefix: '' },
@ -6452,7 +6435,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
private async fetchClientWellKnown(): Promise<void> { private async fetchClientWellKnown(): Promise<void> {
// `getRawClientConfig` does not throw or reject on network errors, instead // `getRawClientConfig` does not throw or reject on network errors, instead
// it absorbs errors and returns `{}`. // it absorbs errors and returns `{}`.
this.clientWellKnownPromise = AutoDiscovery.getRawClientConfig(this.getDomain()); this.clientWellKnownPromise = AutoDiscovery.getRawClientConfig(this.getDomain() ?? undefined);
this.clientWellKnown = await this.clientWellKnownPromise; this.clientWellKnown = await this.clientWellKnownPromise;
this.emit(ClientEvent.ClientWellKnown, this.clientWellKnown); this.emit(ClientEvent.ClientWellKnown, this.clientWellKnown);
} }
@ -6474,7 +6457,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
public storeClientOptions(): Promise<void> { // XXX: Intended private, used in code public storeClientOptions(): Promise<void> { // XXX: Intended private, used in code
const primTypes = ["boolean", "string", "number"]; const primTypes = ["boolean", "string", "number"];
const serializableOpts = Object.entries(this.clientOpts) const serializableOpts = Object.entries(this.clientOpts!)
.filter(([key, value]) => { .filter(([key, value]) => {
return primTypes.includes(typeof value); return primTypes.includes(typeof value);
}) })
@ -6530,7 +6513,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}, },
).catch((e: Error) => { ).catch((e: Error) => {
// Need to unset this if it fails, otherwise we'll never retry // Need to unset this if it fails, otherwise we'll never retry
this.serverVersionsPromise = null; this.serverVersionsPromise = undefined;
// but rethrow the exception to anything that was waiting // but rethrow the exception to anything that was waiting
throw e; throw e;
}); });
@ -6692,7 +6675,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {boolean} Whether or not members are lazy loaded by this client * @return {boolean} Whether or not members are lazy loaded by this client
*/ */
public hasLazyLoadMembersEnabled(): boolean { public hasLazyLoadMembersEnabled(): boolean {
return !!this.clientOpts.lazyLoadMembers; return !!this.clientOpts?.lazyLoadMembers;
} }
/** /**
@ -6712,7 +6695,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Get the callback set via `setCanResetTimelineCallback`. * Get the callback set via `setCanResetTimelineCallback`.
* @return {?Function} The callback or null * @return {?Function} The callback or null
*/ */
public getCanResetTimelineCallback(): ResetTimelineCallback { public getCanResetTimelineCallback(): ResetTimelineCallback | undefined {
return this.canResetTimelineCallback; return this.canResetTimelineCallback;
} }
@ -6734,7 +6717,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
eventType?: EventType | string | null, eventType?: EventType | string | null,
opts: IRelationsRequestOpts = { dir: Direction.Backward }, opts: IRelationsRequestOpts = { dir: Direction.Backward },
): Promise<{ ): Promise<{
originalEvent: MatrixEvent; originalEvent?: MatrixEvent;
events: MatrixEvent[]; events: MatrixEvent[];
nextBatch?: string; nextBatch?: string;
prevBatch?: string; prevBatch?: string;
@ -6775,7 +6758,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* triggering a user interaction. * triggering a user interaction.
* @return {object} * @return {object}
*/ */
public getCrossSigningCacheCallbacks(): ICacheCallbacks { public getCrossSigningCacheCallbacks(): ICacheCallbacks | undefined {
// XXX: Private member access // XXX: Private member access
return this.crypto?.crossSigningInfo.getCacheCallbacks(); return this.crypto?.crossSigningInfo.getCacheCallbacks();
} }
@ -8032,7 +8015,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 getPushRules(): Promise<IPushRules> { public getPushRules(): Promise<IPushRules> {
return this.http.authedRequest(Method.Get, "/pushrules/").then((rules: IPushRules) => { return this.http.authedRequest<IPushRules>(Method.Get, "/pushrules/").then((rules: IPushRules) => {
return PushProcessor.rewriteDefaultRules(rules); return PushProcessor.rewriteDefaultRules(rules);
}); });
} }
@ -8455,8 +8438,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
public getIdentityHashDetails(identityAccessToken: string): Promise<any> { // TODO: Types public getIdentityHashDetails(identityAccessToken: string): Promise<any> { // TODO: Types
return this.http.idServerRequest( return this.http.idServerRequest(
Method.Get, "/hash_details", Method.Get,
null, IdentityPrefix.V2, identityAccessToken, "/hash_details",
undefined,
IdentityPrefix.V2,
identityAccessToken,
); );
} }
@ -8530,7 +8516,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (!response || !response['mappings']) return []; // no results if (!response || !response['mappings']) return []; // no results
const foundAddresses = [/* {address: "plain@example.org", mxid} */]; const foundAddresses: { address: string, mxid: string }[] = [];
for (const hashed of Object.keys(response['mappings'])) { for (const hashed of Object.keys(response['mappings'])) {
const mxid = response['mappings'][hashed]; const mxid = response['mappings'][hashed];
const plainAddress = localMapping[hashed]; const plainAddress = localMapping[hashed];
@ -8560,14 +8546,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public async lookupThreePid( public async lookupThreePid(
medium: string, medium: string,
address: string, address: string,
identityAccessToken?: string, identityAccessToken: string,
): Promise<any> { // TODO: Types ): Promise<any> { // TODO: Types
// Note: we're using the V2 API by calling this function, but our // Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to // function contract requires a V1 response. We therefore have to
// convert it manually. // convert it manually.
const response = await this.identityHashedLookup( const response = await this.identityHashedLookup([[address, medium]], identityAccessToken);
[[address, medium]], identityAccessToken,
);
const result = response.find(p => p.address === address); const result = response.find(p => p.address === address);
if (!result) { if (!result) {
return {}; return {};
@ -8608,7 +8592,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
query.map(p => [p[1], p[0]]), identityAccessToken, query.map(p => [p[1], p[0]]), identityAccessToken,
); );
const v1results = []; const v1results: [medium: string, address: string, mxid: string][] = [];
for (const mapping of response) { for (const mapping of response) {
const originalQuery = query.find(p => p[1] === mapping.address); const originalQuery = query.find(p => p[1] === mapping.address);
if (!originalQuery) { if (!originalQuery) {
@ -8800,7 +8784,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$roomId: roomId, $roomId: roomId,
}); });
const queryParams: Record<string, string | string[]> = { const queryParams: QueryDict = {
suggested_only: String(suggestedOnly), suggested_only: String(suggestedOnly),
max_depth: maxDepth?.toString(), max_depth: maxDepth?.toString(),
from: fromToken, from: fromToken,

View File

@ -114,10 +114,10 @@ export class CrossSigningInfo {
} }
if (expectedPubkey === undefined) { if (expectedPubkey === undefined) {
expectedPubkey = this.getId(type); expectedPubkey = this.getId(type)!;
} }
function validateKey(key: Uint8Array): [string, PkSigning] { function validateKey(key: Uint8Array | null): [string, PkSigning] | undefined {
if (!key) return; if (!key) return;
const signing = new global.Olm.PkSigning(); const signing = new global.Olm.PkSigning();
const gotPubkey = signing.init_with_seed(key); const gotPubkey = signing.init_with_seed(key);
@ -127,7 +127,7 @@ export class CrossSigningInfo {
signing.free(); signing.free();
} }
let privkey; let privkey: Uint8Array | null = null;
if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) { if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey); privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey);
} }
@ -141,7 +141,7 @@ export class CrossSigningInfo {
const result = validateKey(privkey); const result = validateKey(privkey);
if (result) { if (result) {
if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) { if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey); await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey!);
} }
return result; return result;
} }
@ -169,7 +169,7 @@ export class CrossSigningInfo {
* with, or null if it is not present or not encrypted with a trusted * with, or null if it is not present or not encrypted with a trusted
* key * key
*/ */
public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise<Record<string, object>> { public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise<Record<string, object> | null> {
// check what SSSS keys have encrypted the master key (if any) // check what SSSS keys have encrypted the master key (if any)
const stored = await secretStorage.isStored("m.cross_signing.master") || {}; const stored = await secretStorage.isStored("m.cross_signing.master") || {};
// then check which of those SSSS keys have also encrypted the SSK and USK // then check which of those SSSS keys have also encrypted the SSK and USK
@ -213,7 +213,7 @@ export class CrossSigningInfo {
* @param {SecretStorage} secretStorage The secret store using account data * @param {SecretStorage} secretStorage The secret store using account data
* @return {Uint8Array} The private key * @return {Uint8Array} The private key
*/ */
public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array> { public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array | null> {
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`); const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
if (!encodedKey) { if (!encodedKey) {
return null; return null;
@ -233,7 +233,7 @@ export class CrossSigningInfo {
if (!cacheCallbacks) return false; if (!cacheCallbacks) return false;
const types = type ? [type] : ["master", "self_signing", "user_signing"]; const types = type ? [type] : ["master", "self_signing", "user_signing"];
for (const t of types) { for (const t of types) {
if (!await cacheCallbacks.getCrossSigningKeyCache(t)) { if (!await cacheCallbacks.getCrossSigningKeyCache?.(t)) {
return false; return false;
} }
} }
@ -250,7 +250,7 @@ export class CrossSigningInfo {
const cacheCallbacks = this.cacheCallbacks; const cacheCallbacks = this.cacheCallbacks;
if (!cacheCallbacks) return keys; if (!cacheCallbacks) return keys;
for (const type of ["master", "self_signing", "user_signing"]) { for (const type of ["master", "self_signing", "user_signing"]) {
const privKey = await cacheCallbacks.getCrossSigningKeyCache(type); const privKey = await cacheCallbacks.getCrossSigningKeyCache?.(type);
if (!privKey) { if (!privKey) {
continue; continue;
} }
@ -268,7 +268,7 @@ export class CrossSigningInfo {
* *
* @return {string} the ID * @return {string} the ID
*/ */
public getId(type = "master"): string { public getId(type = "master"): string | null {
if (!this.keys[type]) return null; if (!this.keys[type]) return null;
const keyInfo = this.keys[type]; const keyInfo = this.keys[type];
return publicKeyFromKeyInfo(keyInfo); return publicKeyFromKeyInfo(keyInfo);
@ -469,7 +469,7 @@ export class CrossSigningInfo {
} }
} }
public async signUser(key: CrossSigningInfo): Promise<ICrossSigningKey> { public async signUser(key: CrossSigningInfo): Promise<ICrossSigningKey | undefined> {
if (!this.keys.user_signing) { if (!this.keys.user_signing) {
logger.info("No user signing key: not signing user"); logger.info("No user signing key: not signing user");
return; return;
@ -477,7 +477,7 @@ export class CrossSigningInfo {
return this.signObject(key.keys.master, "user_signing"); return this.signObject(key.keys.master, "user_signing");
} }
public async signDevice(userId: string, device: DeviceInfo): Promise<ISignedKey> { public async signDevice(userId: string, device: DeviceInfo): Promise<ISignedKey | undefined> {
if (userId !== this.userId) { if (userId !== this.userId) {
throw new Error( throw new Error(
`Trying to sign ${userId}'s device; can only sign our own device`, `Trying to sign ${userId}'s device; can only sign our own device`,
@ -521,9 +521,9 @@ export class CrossSigningInfo {
return new UserTrustLevel(false, false, userCrossSigning.firstUse); return new UserTrustLevel(false, false, userCrossSigning.firstUse);
} }
let userTrusted; let userTrusted: boolean;
const userMaster = userCrossSigning.keys.master; const userMaster = userCrossSigning.keys.master;
const uskId = this.getId('user_signing'); const uskId = this.getId('user_signing')!;
try { try {
pkVerify(userMaster, uskId, this.userId); pkVerify(userMaster, uskId, this.userId);
userTrusted = true; userTrusted = true;
@ -567,7 +567,7 @@ export class CrossSigningInfo {
const deviceObj = deviceToObject(device, userCrossSigning.userId); const deviceObj = deviceToObject(device, userCrossSigning.userId);
try { try {
// if we can verify the user's SSK from their master key... // if we can verify the user's SSK from their master key...
pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); pkVerify(userSSK, userCrossSigning.getId()!, userCrossSigning.userId);
// ...and this device's key from their SSK... // ...and this device's key from their SSK...
pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId); pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId);
// ...then we trust this device as much as far as we trust the user // ...then we trust this device as much as far as we trust the user
@ -752,7 +752,7 @@ export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning],
* @param {string} userId The user ID being verified * @param {string} userId The user ID being verified
* @param {string} deviceId The device ID being verified * @param {string} deviceId The device ID being verified
*/ */
export function requestKeysDuringVerification( export async function requestKeysDuringVerification(
baseApis: MatrixClient, baseApis: MatrixClient,
userId: string, userId: string,
deviceId: string, deviceId: string,
@ -766,7 +766,7 @@ export function requestKeysDuringVerification(
// it. We return here in order to test. // it. We return here in order to test.
return new Promise<KeysDuringVerification | void>((resolve, reject) => { return new Promise<KeysDuringVerification | void>((resolve, reject) => {
const client = baseApis; const client = baseApis;
const original = client.crypto.crossSigningInfo; const original = client.crypto!.crossSigningInfo;
// We already have all of the infrastructure we need to validate and // We already have all of the infrastructure we need to validate and
// cache cross-signing keys, so instead of replicating that, here we set // cache cross-signing keys, so instead of replicating that, here we set
@ -801,7 +801,7 @@ export function requestKeysDuringVerification(
// also request and cache the key backup key // also request and cache the key backup key
const backupKeyPromise = (async () => { const backupKeyPromise = (async () => {
const cachedKey = await client.crypto.getSessionBackupPrivateKey(); const cachedKey = await client.crypto!.getSessionBackupPrivateKey();
if (!cachedKey) { if (!cachedKey) {
logger.info("No cached backup key found. Requesting..."); logger.info("No cached backup key found. Requesting...");
const secretReq = client.requestSecret( const secretReq = client.requestSecret(
@ -811,13 +811,13 @@ export function requestKeysDuringVerification(
logger.info("Got key backup key, decoding..."); logger.info("Got key backup key, decoding...");
const decodedKey = decodeBase64(base64Key); const decodedKey = decodeBase64(base64Key);
logger.info("Decoded backup key, storing..."); logger.info("Decoded backup key, storing...");
await client.crypto.storeSessionBackupPrivateKey( await client.crypto!.storeSessionBackupPrivateKey(
Uint8Array.from(decodedKey), Uint8Array.from(decodedKey),
); );
logger.info("Backup key stored. Starting backup restore..."); logger.info("Backup key stored. Starting backup restore...");
const backupInfo = await client.getKeyBackupVersion(); const backupInfo = await client.getKeyBackupVersion();
// no need to await for this - just let it go in the bg // no need to await for this - just let it go in the bg
client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => { client.restoreKeyBackupWithCache(undefined, undefined, backupInfo!).then(() => {
logger.info("Backup restored."); logger.info("Backup restored.");
}); });
} }

View File

@ -26,7 +26,7 @@ import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
import * as olmlib from './olmlib'; import * as olmlib from './olmlib';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { chunkPromises, defer, IDeferred, sleep } from '../utils'; import { chunkPromises, defer, IDeferred, sleep } from '../utils';
import { IDownloadKeyResult, MatrixClient } from "../client"; import { DeviceKeys, IDownloadKeyResult, Keys, MatrixClient, SigningKeys } from "../client";
import { OlmDevice } from "./OlmDevice"; import { OlmDevice } from "./OlmDevice";
import { CryptoStore } from "./store/base"; import { CryptoStore } from "./store/base";
import { TypedEventEmitter } from "../models/typed-event-emitter"; import { TypedEventEmitter } from "../models/typed-event-emitter";
@ -81,24 +81,24 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
// The 'next_batch' sync token at the point the data was written, // The 'next_batch' sync token at the point the data was written,
// ie. a token representing the point immediately after the // ie. a token representing the point immediately after the
// moment represented by the snapshot in the db. // moment represented by the snapshot in the db.
private syncToken: string = null; private syncToken: string | null = null;
private keyDownloadsInProgressByUser: { [userId: string]: Promise<void> } = {}; private keyDownloadsInProgressByUser = new Map<string, Promise<void>>;
// Set whenever changes are made other than setting the sync token // Set whenever changes are made other than setting the sync token
private dirty = false; private dirty = false;
// Promise resolved when device data is saved // Promise resolved when device data is saved
private savePromise: Promise<boolean> = null; private savePromise: Promise<boolean> | null = null;
// Function that resolves the save promise // Function that resolves the save promise
private resolveSavePromise: (saved: boolean) => void = null; private resolveSavePromise: ((saved: boolean) => void) | null = null;
// The time the save is scheduled for // The time the save is scheduled for
private savePromiseTime: number = null; private savePromiseTime: number | null = null;
// The timer used to delay the save // The timer used to delay the save
private saveTimer: ReturnType<typeof setTimeout> = null; private saveTimer: ReturnType<typeof setTimeout> | null = null;
// True if we have fetched data from the server or loaded a non-empty // True if we have fetched data from the server or loaded a non-empty
// set of device data from the store // set of device data from the store
private hasFetched: boolean = null; private hasFetched: boolean | null = null;
private readonly serialiser: DeviceListUpdateSerialiser; private readonly serialiser: DeviceListUpdateSerialiser;
@ -127,7 +127,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
deviceData.crossSigningInfo || {} : {}; deviceData.crossSigningInfo || {} : {};
this.deviceTrackingStatus = deviceData ? this.deviceTrackingStatus = deviceData ?
deviceData.trackingStatus : {}; deviceData.trackingStatus : {};
this.syncToken = deviceData ? deviceData.syncToken : null; this.syncToken = deviceData?.syncToken ?? null;
this.userByIdentityKey = {}; this.userByIdentityKey = {};
for (const user of Object.keys(this.devices)) { for (const user of Object.keys(this.devices)) {
const userDevices = this.devices[user]; const userDevices = this.devices[user];
@ -181,7 +181,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
if (this.savePromiseTime && targetTime < this.savePromiseTime) { if (this.savePromiseTime && targetTime < this.savePromiseTime) {
// There's a save scheduled but for after we would like: cancel // There's a save scheduled but for after we would like: cancel
// it & schedule one for the time we want // it & schedule one for the time we want
clearTimeout(this.saveTimer); clearTimeout(this.saveTimer!);
this.saveTimer = null; this.saveTimer = null;
this.savePromiseTime = null; this.savePromiseTime = null;
// (but keep the save promise since whatever called save before // (but keep the save promise since whatever called save before
@ -216,13 +216,13 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
devices: this.devices, devices: this.devices,
crossSigningInfo: this.crossSigningInfo, crossSigningInfo: this.crossSigningInfo,
trackingStatus: this.deviceTrackingStatus, trackingStatus: this.deviceTrackingStatus,
syncToken: this.syncToken, syncToken: this.syncToken ?? undefined,
}, txn); }, txn);
}, },
).then(() => { ).then(() => {
// The device list is considered dirty until the write completes. // The device list is considered dirty until the write completes.
this.dirty = false; this.dirty = false;
resolveSavePromise(true); resolveSavePromise?.(true);
}, err => { }, err => {
logger.error('Failed to save device tracking data', this.syncToken); logger.error('Failed to save device tracking data', this.syncToken);
logger.error(err); logger.error(err);
@ -230,7 +230,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
}, delay); }, delay);
} }
return savePromise; return savePromise!;
} }
/** /**
@ -238,7 +238,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* *
* @return {string} The sync token * @return {string} The sync token
*/ */
public getSyncToken(): string { public getSyncToken(): string | null {
return this.syncToken; return this.syncToken;
} }
@ -272,14 +272,14 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
userIds.forEach((u) => { userIds.forEach((u) => {
const trackingStatus = this.deviceTrackingStatus[u]; const trackingStatus = this.deviceTrackingStatus[u];
if (this.keyDownloadsInProgressByUser[u]) { if (this.keyDownloadsInProgressByUser.has(u)) {
// already a key download in progress/queued for this user; its results // already a key download in progress/queued for this user; its results
// will be good enough for us. // will be good enough for us.
logger.log( logger.log(
`downloadKeys: already have a download in progress for ` + `downloadKeys: already have a download in progress for ` +
`${u}: awaiting its result`, `${u}: awaiting its result`,
); );
promises.push(this.keyDownloadsInProgressByUser[u]); promises.push(this.keyDownloadsInProgressByUser.get(u)!);
} else if (forceDownload || trackingStatus != TrackingStatus.UpToDate) { } else if (forceDownload || trackingStatus != TrackingStatus.UpToDate) {
usersToDownload.push(u); usersToDownload.push(u);
} }
@ -341,7 +341,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
if (!devs) { if (!devs) {
return null; return null;
} }
const res = []; const res: DeviceInfo[] = [];
for (const deviceId in devs) { for (const deviceId in devs) {
if (devs.hasOwnProperty(deviceId)) { if (devs.hasOwnProperty(deviceId)) {
res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId)); res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
@ -362,7 +362,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
return this.devices[userId]; return this.devices[userId];
} }
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo { public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
if (!this.crossSigningInfo[userId]) return null; if (!this.crossSigningInfo[userId]) return null;
return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId); return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId);
@ -382,9 +382,9 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* @return {module:crypto/deviceinfo?} device, or undefined * @return {module:crypto/deviceinfo?} device, or undefined
* if we don't know about this device * if we don't know about this device
*/ */
public getStoredDevice(userId: string, deviceId: string): DeviceInfo { public getStoredDevice(userId: string, deviceId: string): DeviceInfo | undefined {
const devs = this.devices[userId]; const devs = this.devices[userId];
if (!devs || !devs[deviceId]) { if (!devs?.[deviceId]) {
return undefined; return undefined;
} }
return DeviceInfo.fromStorage(devs[deviceId], deviceId); return DeviceInfo.fromStorage(devs[deviceId], deviceId);
@ -398,11 +398,8 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* *
* @return {string} user ID * @return {string} user ID
*/ */
public getUserByIdentityKey(algorithm: string, senderKey: string): string { public getUserByIdentityKey(algorithm: string, senderKey: string): string | null {
if ( if (algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM) {
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
) {
// we only deal in olm keys // we only deal in olm keys
return null; return null;
} }
@ -557,7 +554,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
public refreshOutdatedDeviceLists(): Promise<void> { public refreshOutdatedDeviceLists(): Promise<void> {
this.saveIfDirty(); this.saveIfDirty();
const usersToDownload = []; const usersToDownload: string[] = [];
for (const userId of Object.keys(this.deviceTrackingStatus)) { for (const userId of Object.keys(this.deviceTrackingStatus)) {
const stat = this.deviceTrackingStatus[userId]; const stat = this.deviceTrackingStatus[userId];
if (stat == TrackingStatus.PendingDownload) { if (stat == TrackingStatus.PendingDownload) {
@ -617,7 +614,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
return Promise.resolve(); return Promise.resolve();
} }
const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken).then(() => { const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken!).then(() => {
finished(true); finished(true);
}, (e) => { }, (e) => {
logger.error( logger.error(
@ -628,7 +625,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
}); });
users.forEach((u) => { users.forEach((u) => {
this.keyDownloadsInProgressByUser[u] = prom; this.keyDownloadsInProgressByUser.set(u, prom);
const stat = this.deviceTrackingStatus[u]; const stat = this.deviceTrackingStatus[u];
if (stat == TrackingStatus.PendingDownload) { if (stat == TrackingStatus.PendingDownload) {
this.deviceTrackingStatus[u] = TrackingStatus.DownloadInProgress; this.deviceTrackingStatus[u] = TrackingStatus.DownloadInProgress;
@ -643,11 +640,11 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
// we may have queued up another download request for this user // we may have queued up another download request for this user
// since we started this request. If that happens, we should // since we started this request. If that happens, we should
// ignore the completion of the first one. // ignore the completion of the first one.
if (this.keyDownloadsInProgressByUser[u] !== prom) { if (this.keyDownloadsInProgressByUser.get(u) !== prom) {
logger.log('Another update in the queue for', u, '- not marking up-to-date'); logger.log('Another update in the queue for', u, '- not marking up-to-date');
return; return;
} }
delete this.keyDownloadsInProgressByUser[u]; this.keyDownloadsInProgressByUser.delete(u);
const stat = this.deviceTrackingStatus[u]; const stat = this.deviceTrackingStatus[u];
if (stat == TrackingStatus.DownloadInProgress) { if (stat == TrackingStatus.DownloadInProgress) {
if (success) { if (success) {
@ -687,9 +684,9 @@ class DeviceListUpdateSerialiser {
// deferred which is resolved when the queued users are downloaded. // deferred which is resolved when the queued users are downloaded.
// non-null indicates that we have users queued for download. // non-null indicates that we have users queued for download.
private queuedQueryDeferred: IDeferred<void> = null; private queuedQueryDeferred?: IDeferred<void>;
private syncToken: string = null; // The sync token we send with the requests private syncToken?: string; // The sync token we send with the requests
/* /*
* @param {object} baseApis Base API object * @param {object} baseApis Base API object
@ -748,7 +745,7 @@ class DeviceListUpdateSerialiser {
const downloadUsers = Object.keys(this.keyDownloadsQueuedByUser); const downloadUsers = Object.keys(this.keyDownloadsQueuedByUser);
this.keyDownloadsQueuedByUser = {}; this.keyDownloadsQueuedByUser = {};
const deferred = this.queuedQueryDeferred; const deferred = this.queuedQueryDeferred;
this.queuedQueryDeferred = null; this.queuedQueryDeferred = undefined;
logger.log('Starting key download for', downloadUsers); logger.log('Starting key download for', downloadUsers);
this.downloadInProgress = true; this.downloadInProgress = true;
@ -785,9 +782,9 @@ class DeviceListUpdateSerialiser {
try { try {
await this.processQueryResponseForUser( await this.processQueryResponseForUser(
userId, dk[userId], { userId, dk[userId], {
master: masterKeys[userId], master: masterKeys?.[userId],
self_signing: ssks[userId], self_signing: ssks?.[userId],
user_signing: usks[userId], user_signing: usks?.[userId],
}, },
); );
} catch (e) { } catch (e) {
@ -800,7 +797,7 @@ class DeviceListUpdateSerialiser {
logger.log('Completed key download for ' + downloadUsers); logger.log('Completed key download for ' + downloadUsers);
this.downloadInProgress = false; this.downloadInProgress = false;
deferred.resolve(); deferred?.resolve();
// if we have queued users, fire off another request. // if we have queued users, fire off another request.
if (this.queuedQueryDeferred) { if (this.queuedQueryDeferred) {
@ -809,19 +806,19 @@ class DeviceListUpdateSerialiser {
}, (e) => { }, (e) => {
logger.warn('Error downloading keys for ' + downloadUsers + ':', e); logger.warn('Error downloading keys for ' + downloadUsers + ':', e);
this.downloadInProgress = false; this.downloadInProgress = false;
deferred.reject(e); deferred?.reject(e);
}); });
return deferred.promise; return deferred!.promise;
} }
private async processQueryResponseForUser( private async processQueryResponseForUser(
userId: string, userId: string,
dkResponse: IDownloadKeyResult["device_keys"]["user_id"], dkResponse: DeviceKeys,
crossSigningResponse: { crossSigningResponse: {
master: IDownloadKeyResult["master_keys"]["user_id"]; master?: Keys;
self_signing: IDownloadKeyResult["master_keys"]["user_id"]; // eslint-disable-line camelcase self_signing?: SigningKeys;
user_signing: IDownloadKeyResult["user_signing_keys"]["user_id"]; // eslint-disable-line camelcase user_signing?: SigningKeys;
}, },
): Promise<void> { ): Promise<void> {
logger.log('got device keys for ' + userId + ':', dkResponse); logger.log('got device keys for ' + userId + ':', dkResponse);
@ -840,7 +837,7 @@ class DeviceListUpdateSerialiser {
await updateStoredDeviceKeysForUser( await updateStoredDeviceKeysForUser(
this.olmDevice, userId, userStore, dkResponse || {}, this.olmDevice, userId, userStore, dkResponse || {},
this.baseApis.getUserId(), this.baseApis.deviceId, this.baseApis.getUserId()!, this.baseApis.deviceId!,
); );
// put the updates into the object that will be returned as our results // put the updates into the object that will be returned as our results

View File

@ -15,9 +15,8 @@ limitations under the License.
*/ */
import { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } from "@matrix-org/olm"; import { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } from "@matrix-org/olm";
import { Logger } from "loglevel";
import { logger } from '../logger'; import { logger, PrefixedLogger } from '../logger';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import * as algorithms from './algorithms'; import * as algorithms from './algorithms';
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base"; import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
@ -121,9 +120,9 @@ interface IInboundGroupSessionKey {
chain_index: number; chain_index: number;
key: string; key: string;
forwarding_curve25519_key_chain: string[]; forwarding_curve25519_key_chain: string[];
sender_claimed_ed25519_key: string; sender_claimed_ed25519_key: string | null;
shared_history: boolean; shared_history: boolean;
untrusted: boolean; untrusted?: boolean;
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
@ -145,9 +144,9 @@ export class OlmDevice {
public pickleKey = "DEFAULT_KEY"; // set by consumers public pickleKey = "DEFAULT_KEY"; // set by consumers
// don't know these until we load the account from storage in init() // don't know these until we load the account from storage in init()
public deviceCurve25519Key: string = null; public deviceCurve25519Key: string | null = null;
public deviceEd25519Key: string = null; public deviceEd25519Key: string | null = null;
private maxOneTimeKeys: number = null; private maxOneTimeKeys: number | null = null;
// we don't bother stashing outboundgroupsessions in the cryptoStore - // we don't bother stashing outboundgroupsessions in the cryptoStore -
// instead we keep them here. // instead we keep them here.
@ -266,8 +265,8 @@ export class OlmDevice {
lastReceivedMessageTs: session.lastReceivedMessageTs, lastReceivedMessageTs: session.lastReceivedMessageTs,
}; };
this.cryptoStore.storeEndToEndSession( this.cryptoStore.storeEndToEndSession(
deviceKey, deviceKey!,
sessionId, sessionId!,
sessionInfo, sessionInfo,
txn, txn,
); );
@ -358,7 +357,7 @@ export class OlmDevice {
// is not exactly the same thing you get in method _getSession // is not exactly the same thing you get in method _getSession
// see documentation of IndexedDBCryptoStore.getAllEndToEndSessions // see documentation of IndexedDBCryptoStore.getAllEndToEndSessions
this.cryptoStore.getAllEndToEndSessions(txn, (pickledSession) => { this.cryptoStore.getAllEndToEndSessions(txn, (pickledSession) => {
result.sessions.push(pickledSession); result.sessions!.push(pickledSession!);
}); });
}, },
); );
@ -384,8 +383,8 @@ export class OlmDevice {
func: (unpickledSessionInfo: IUnpickledSessionInfo) => void, func: (unpickledSessionInfo: IUnpickledSessionInfo) => void,
): void { ): void {
this.cryptoStore.getEndToEndSession( this.cryptoStore.getEndToEndSession(
deviceKey, sessionId, txn, (sessionInfo: ISessionInfo) => { deviceKey, sessionId, txn, (sessionInfo: ISessionInfo | null) => {
this.unpickleSession(sessionInfo, func); this.unpickleSession(sessionInfo!, func);
}, },
); );
} }
@ -405,7 +404,7 @@ export class OlmDevice {
): void { ): void {
const session = new global.Olm.Session(); const session = new global.Olm.Session();
try { try {
session.unpickle(this.pickleKey, sessionInfo.session); session.unpickle(this.pickleKey, sessionInfo.session!);
const unpickledSessInfo: IUnpickledSessionInfo = Object.assign({}, sessionInfo, { session }); const unpickledSessInfo: IUnpickledSessionInfo = Object.assign({}, sessionInfo, { session });
func(unpickledSessInfo); func(unpickledSessInfo);
@ -491,7 +490,7 @@ export class OlmDevice {
* @return {number} number of keys * @return {number} number of keys
*/ */
public maxNumberOfOneTimeKeys(): number { public maxNumberOfOneTimeKeys(): number {
return this.maxOneTimeKeys; return this.maxOneTimeKeys ?? -1;
} }
/** /**
@ -554,7 +553,7 @@ export class OlmDevice {
}); });
}, },
); );
return result; return result!;
} }
public async forgetOldFallbackKey(): Promise<void> { public async forgetOldFallbackKey(): Promise<void> {
@ -607,7 +606,7 @@ export class OlmDevice {
}, },
logger.withPrefix("[createOutboundSession]"), logger.withPrefix("[createOutboundSession]"),
); );
return newSessionId; return newSessionId!;
} }
/** /**
@ -668,7 +667,7 @@ export class OlmDevice {
logger.withPrefix("[createInboundSession]"), logger.withPrefix("[createInboundSession]"),
); );
return result; return result!;
} }
/** /**
@ -703,7 +702,7 @@ export class OlmDevice {
log, log,
); );
return sessionIds; return sessionIds!;
} }
/** /**
@ -714,13 +713,13 @@ export class OlmDevice {
* @param {boolean} nowait Don't wait for an in-progress session to complete. * @param {boolean} nowait Don't wait for an in-progress session to complete.
* This should only be set to true of the calling function is the function * This should only be set to true of the calling function is the function
* that marked the session as being in-progress. * that marked the session as being in-progress.
* @param {Logger} [log] A possibly customised log * @param {PrefixedLogger} [log] A possibly customised log
* @return {Promise<?string>} session id, or null if no established session * @return {Promise<?string>} session id, or null if no established session
*/ */
public async getSessionIdForDevice( public async getSessionIdForDevice(
theirDeviceIdentityKey: string, theirDeviceIdentityKey: string,
nowait = false, nowait = false,
log?: Logger, log?: PrefixedLogger,
): Promise<string | null> { ): Promise<string | null> {
const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey, nowait, log); const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey, nowait, log);
@ -780,7 +779,11 @@ export class OlmDevice {
// return an empty result // return an empty result
} }
} }
const info = []; const info: {
lastReceivedMessageTs: number;
hasReceivedMessage: boolean;
sessionId: string;
}[] = [];
await this.cryptoStore.doTxn( await this.cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_SESSIONS], 'readonly', [IndexedDBCryptoStore.STORE_SESSIONS],
@ -790,9 +793,9 @@ export class OlmDevice {
for (const sessionId of sessionIds) { for (const sessionId of sessionIds) {
this.unpickleSession(sessions[sessionId], (sessInfo: IUnpickledSessionInfo) => { this.unpickleSession(sessions[sessionId], (sessInfo: IUnpickledSessionInfo) => {
info.push({ info.push({
lastReceivedMessageTs: sessInfo.lastReceivedMessageTs, lastReceivedMessageTs: sessInfo.lastReceivedMessageTs!,
hasReceivedMessage: sessInfo.session.has_received_message(), hasReceivedMessage: sessInfo.session.has_received_message(),
sessionId: sessionId, sessionId,
}); });
}); });
} }
@ -801,7 +804,7 @@ export class OlmDevice {
log, log,
); );
return info; return info!;
} }
/** /**
@ -916,7 +919,7 @@ export class OlmDevice {
await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed); await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
} }
public sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise<IProblem> { public sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise<IProblem | null> {
return this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp); return this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
} }
@ -1056,10 +1059,14 @@ export class OlmDevice {
senderKey: string, senderKey: string,
sessionId: string, sessionId: string,
txn: unknown, txn: unknown,
func: (session: InboundGroupSession, data: InboundGroupSessionData, withheld?: IWithheld) => void, func: (
session: InboundGroupSession | null,
data: InboundGroupSessionData | null,
withheld: IWithheld | null,
) => void,
): void { ): void {
this.cryptoStore.getEndToEndInboundGroupSession( this.cryptoStore.getEndToEndInboundGroupSession(
senderKey, sessionId, txn, (sessionData: InboundGroupSessionData, withheld: IWithheld | null) => { senderKey, sessionId, txn, (sessionData: InboundGroupSessionData | null, withheld: IWithheld | null) => {
if (sessionData === null) { if (sessionData === null) {
func(null, null, withheld); func(null, null, withheld);
return; return;
@ -1112,94 +1119,94 @@ export class OlmDevice {
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
], (txn) => { ], (txn) => {
/* if we already have this session, consider updating it */ /* if we already have this session, consider updating it */
this.getInboundGroupSession( this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (
roomId, senderKey, sessionId, txn, existingSession: InboundGroupSession | null,
(existingSession: InboundGroupSession, existingSessionData: InboundGroupSessionData) => { existingSessionData: InboundGroupSessionData | null,
// new session. ) => {
const session = new global.Olm.InboundGroupSession(); // new session.
try { const session = new global.Olm.InboundGroupSession();
if (exportFormat) { try {
session.import_session(sessionKey); if (exportFormat) {
} else { session.import_session(sessionKey);
session.create(sessionKey); } else {
} session.create(sessionKey);
if (sessionId != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " +
senderKey,
);
}
if (existingSession) {
logger.log(
"Update for megolm session "
+ senderKey + "/" + sessionId,
);
if (existingSession.first_known_index() <= session.first_known_index()) {
if (!existingSessionData.untrusted || extraSessionData.untrusted) {
// existing session has less-than-or-equal index
// (i.e. can decrypt at least as much), and the
// new session's trust does not win over the old
// session's trust, so keep it
logger.log(`Keeping existing megolm session ${sessionId}`);
return;
}
if (existingSession.first_known_index() < session.first_known_index()) {
// We want to upgrade the existing session's trust,
// but we can't just use the new session because we'll
// lose the lower index. Check that the sessions connect
// properly, and then manually set the existing session
// as trusted.
if (
existingSession.export_session(session.first_known_index())
=== session.export_session(session.first_known_index())
) {
logger.info(
"Upgrading trust of existing megolm session " +
sessionId + " based on newly-received trusted session",
);
existingSessionData.untrusted = false;
this.cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, existingSessionData, txn,
);
} else {
logger.warn(
"Newly-received megolm session " + sessionId +
" does not match existing session! Keeping existing session",
);
}
return;
}
// If the sessions have the same index, go ahead and store the new trusted one.
}
}
logger.info(
"Storing megolm session " + senderKey + "/" + sessionId +
" with first index " + session.first_known_index(),
);
const sessionData = Object.assign({}, extraSessionData, {
room_id: roomId,
session: session.pickle(this.pickleKey),
keysClaimed: keysClaimed,
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
});
this.cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn,
);
if (!existingSession && extraSessionData.sharedHistory) {
this.cryptoStore.addSharedHistoryInboundGroupSession(
roomId, senderKey, sessionId, txn,
);
}
} finally {
session.free();
} }
}, if (sessionId != session.session_id()) {
); throw new Error(
"Mismatched group session ID from senderKey: " +
senderKey,
);
}
if (existingSession) {
logger.log(
"Update for megolm session "
+ senderKey + "/" + sessionId,
);
if (existingSession.first_known_index() <= session.first_known_index()) {
if (!existingSessionData!.untrusted || extraSessionData.untrusted) {
// existing session has less-than-or-equal index
// (i.e. can decrypt at least as much), and the
// new session's trust does not win over the old
// session's trust, so keep it
logger.log(`Keeping existing megolm session ${sessionId}`);
return;
}
if (existingSession.first_known_index() < session.first_known_index()) {
// We want to upgrade the existing session's trust,
// but we can't just use the new session because we'll
// lose the lower index. Check that the sessions connect
// properly, and then manually set the existing session
// as trusted.
if (
existingSession.export_session(session.first_known_index())
=== session.export_session(session.first_known_index())
) {
logger.info(
"Upgrading trust of existing megolm session " +
sessionId + " based on newly-received trusted session",
);
existingSessionData!.untrusted = false;
this.cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, existingSessionData!, txn,
);
} else {
logger.warn(
"Newly-received megolm session " + sessionId +
" does not match existing session! Keeping existing session",
);
}
return;
}
// If the sessions have the same index, go ahead and store the new trusted one.
}
}
logger.info(
"Storing megolm session " + senderKey + "/" + sessionId +
" with first index " + session.first_known_index(),
);
const sessionData = Object.assign({}, extraSessionData, {
room_id: roomId,
session: session.pickle(this.pickleKey),
keysClaimed: keysClaimed,
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
});
this.cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn,
);
if (!existingSession && extraSessionData.sharedHistory) {
this.cryptoStore.addSharedHistoryInboundGroupSession(
roomId, senderKey, sessionId, txn,
);
}
} finally {
session.free();
}
});
}, },
logger.withPrefix("[addInboundGroupSession]"), logger.withPrefix("[addInboundGroupSession]"),
); );
@ -1261,7 +1268,7 @@ export class OlmDevice {
eventId: string, eventId: string,
timestamp: number, timestamp: number,
): Promise<IDecryptedGroupMessage | null> { ): Promise<IDecryptedGroupMessage | null> {
let result: IDecryptedGroupMessage; let result: IDecryptedGroupMessage | null = null;
// when the localstorage crypto store is used as an indexeddb backend, // when the localstorage crypto store is used as an indexeddb backend,
// exceptions thrown from within the inner function are not passed through // exceptions thrown from within the inner function are not passed through
// to the top level, so we store exceptions in a variable and raise them at // to the top level, so we store exceptions in a variable and raise them at
@ -1275,7 +1282,7 @@ export class OlmDevice {
], (txn) => { ], (txn) => {
this.getInboundGroupSession( this.getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => { roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
if (session === null) { if (session === null || sessionData === null) {
if (withheld) { if (withheld) {
error = new algorithms.DecryptionError( error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID", "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
@ -1292,7 +1299,7 @@ export class OlmDevice {
try { try {
res = session.decrypt(body); res = session.decrypt(body);
} catch (e) { } catch (e) {
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) { if ((<Error>e)?.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) {
error = new algorithms.DecryptionError( error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID", "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
calculateWithheldMessage(withheld), calculateWithheldMessage(withheld),
@ -1301,7 +1308,7 @@ export class OlmDevice {
}, },
); );
} else { } else {
error = e; error = <Error>e;
} }
return; return;
} }
@ -1350,7 +1357,7 @@ export class OlmDevice {
forwardingCurve25519KeyChain: ( forwardingCurve25519KeyChain: (
sessionData.forwardingCurve25519KeyChain || [] sessionData.forwardingCurve25519KeyChain || []
), ),
untrusted: sessionData.untrusted, untrusted: !!sessionData.untrusted,
}; };
}, },
); );
@ -1358,10 +1365,10 @@ export class OlmDevice {
logger.withPrefix("[decryptGroupMessage]"), logger.withPrefix("[decryptGroupMessage]"),
); );
if (error) { if (error!) {
throw error; throw error;
} }
return result; return result!;
} }
/** /**
@ -1404,7 +1411,7 @@ export class OlmDevice {
logger.withPrefix("[hasInboundSessionKeys]"), logger.withPrefix("[hasInboundSessionKeys]"),
); );
return result; return result!;
} }
/** /**
@ -1431,8 +1438,8 @@ export class OlmDevice {
senderKey: string, senderKey: string,
sessionId: string, sessionId: string,
chainIndex?: number, chainIndex?: number,
): Promise<IInboundGroupSessionKey> { ): Promise<IInboundGroupSessionKey | null> {
let result: IInboundGroupSessionKey; let result: IInboundGroupSessionKey | null = null;
await this.cryptoStore.doTxn( await this.cryptoStore.doTxn(
'readonly', [ 'readonly', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
@ -1440,7 +1447,7 @@ export class OlmDevice {
], (txn) => { ], (txn) => {
this.getInboundGroupSession( this.getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData) => { roomId, senderKey, sessionId, txn, (session, sessionData) => {
if (session === null) { if (session === null || sessionData === null) {
result = null; result = null;
return; return;
} }
@ -1520,7 +1527,7 @@ export class OlmDevice {
}, },
logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"), logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"),
); );
return result; return result!;
} }
// Utilities // Utilities

View File

@ -182,7 +182,7 @@ export class SecretStorage {
* the form [keyId, keyInfo]. Otherwise, null is returned. * the form [keyId, keyInfo]. Otherwise, null is returned.
* XXX: why is this an array when addKey returns an object? * XXX: why is this an array when addKey returns an object?
*/ */
public async getKey(keyId: string): Promise<SecretStorageKeyTuple | null> { public async getKey(keyId?: string | null): Promise<SecretStorageKeyTuple | null> {
if (!keyId) { if (!keyId) {
keyId = await this.getDefaultKeyId(); keyId = await this.getDefaultKeyId();
} }
@ -237,7 +237,7 @@ export class SecretStorage {
* @param {Array} keys The IDs of the keys to use to encrypt the secret * @param {Array} keys The IDs of the keys to use to encrypt the secret
* or null/undefined to use the default key. * or null/undefined to use the default key.
*/ */
public async store(name: string, secret: string, keys?: string[]): Promise<void> { public async store(name: string, secret: string, keys?: string[] | null): Promise<void> {
const encrypted: Record<string, IEncryptedPayload> = {}; const encrypted: Record<string, IEncryptedPayload> = {};
if (!keys) { if (!keys) {
@ -284,7 +284,7 @@ export class SecretStorage {
* *
* @return {string} the contents of the secret * @return {string} the contents of the secret
*/ */
public async get(name: string): Promise<string> { public async get(name: string): Promise<string | undefined> {
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name); const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo) { if (!secretInfo) {
return; return;

View File

@ -242,7 +242,7 @@ export abstract class DecryptionAlgorithm {
export class DecryptionError extends Error { export class DecryptionError extends Error {
public readonly detailedString: string; public readonly detailedString: string;
constructor(public readonly code: string, msg: string, details?: Record<string, string>) { constructor(public readonly code: string, msg: string, details?: Record<string, string | Error>) {
super(msg); super(msg);
this.code = code; this.code = code;
this.name = 'DecryptionError'; this.name = 'DecryptionError';
@ -250,7 +250,7 @@ export class DecryptionError extends Error {
} }
} }
function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string>): string { function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string | Error>): string {
let result = err.name + '[msg: ' + err.message; let result = err.name + '[msg: ' + err.message;
if (details) { if (details) {
@ -272,7 +272,11 @@ function detailedStringForDecryptionError(err: DecryptionError, details?: Record
* @extends Error * @extends Error
*/ */
export class UnknownDeviceError extends Error { export class UnknownDeviceError extends Error {
constructor(msg: string, public readonly devices: Record<string, Record<string, object>>) { constructor(
msg: string,
public readonly devices: Record<string, Record<string, object>>,
public event?: MatrixEvent,
) {
super(msg); super(msg);
this.name = "UnknownDeviceError"; this.name = "UnknownDeviceError";
this.devices = devices; this.devices = devices;
@ -295,7 +299,7 @@ export class UnknownDeviceError extends Error {
export function registerAlgorithm( export function registerAlgorithm(
algorithm: string, algorithm: string,
encryptor: new (params: IParams) => EncryptionAlgorithm, encryptor: new (params: IParams) => EncryptionAlgorithm,
decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm, decryptor: new (params: DecryptionClassParams) => DecryptionAlgorithm,
): void { ): void {
ENCRYPTION_CLASSES.set(algorithm, encryptor); ENCRYPTION_CLASSES.set(algorithm, encryptor);
DECRYPTION_CLASSES.set(algorithm, decryptor); DECRYPTION_CLASSES.set(algorithm, decryptor);

View File

@ -40,6 +40,7 @@ import { EventType, MsgType } from '../../@types/event';
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index"; import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } 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";
// determine whether the key can be shared with invitees // determine whether the key can be shared with invitees
export function isRoomSharedHistory(room: Room): boolean { export function isRoomSharedHistory(room: Room): boolean {
@ -492,13 +493,13 @@ class MegolmEncryption extends EncryptionAlgorithm {
const key = this.olmDevice.getOutboundGroupSessionKey(sessionId); const key = this.olmDevice.getOutboundGroupSessionKey(sessionId);
await this.olmDevice.addInboundGroupSession( await this.olmDevice.addInboundGroupSession(
this.roomId, this.olmDevice.deviceCurve25519Key, [], sessionId, this.roomId, this.olmDevice.deviceCurve25519Key!, [], sessionId,
key.key, { ed25519: this.olmDevice.deviceEd25519Key }, false, key.key, { ed25519: this.olmDevice.deviceEd25519Key! }, false,
{ sharedHistory }, { sharedHistory },
); );
// don't wait for it to complete // don't wait for it to complete
this.crypto.backupManager.backupGroupSession(this.olmDevice.deviceCurve25519Key, sessionId); this.crypto.backupManager.backupGroupSession(this.olmDevice.deviceCurve25519Key!, sessionId);
return new OutboundSessionInfo(sessionId, sharedHistory); return new OutboundSessionInfo(sessionId, sharedHistory);
} }
@ -929,7 +930,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
room_id: this.roomId, room_id: this.roomId,
session_id: session.sessionId, session_id: session.sessionId,
algorithm: olmlib.MEGOLM_ALGORITHM, algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key, sender_key: this.olmDevice.deviceCurve25519Key!,
}; };
const userDeviceMaps = this.splitDevices(devicesByUser); const userDeviceMaps = this.splitDevices(devicesByUser);
@ -1259,21 +1260,21 @@ class MegolmDecryption extends DecryptionAlgorithm {
// (fixes https://github.com/vector-im/element-web/issues/5001) // (fixes https://github.com/vector-im/element-web/issues/5001)
this.addEventToPendingList(event); this.addEventToPendingList(event);
let res: IDecryptedGroupMessage; let res: IDecryptedGroupMessage | null;
try { try {
res = await this.olmDevice.decryptGroupMessage( res = await this.olmDevice.decryptGroupMessage(
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext, event.getRoomId()!, content.sender_key, content.session_id, content.ciphertext,
event.getId(), event.getTs(), event.getId(), event.getTs(),
); );
} catch (e) { } catch (e) {
if (e.name === "DecryptionError") { if ((<Error>e).name === "DecryptionError") {
// re-throw decryption errors as-is // re-throw decryption errors as-is
throw e; throw e;
} }
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR"; let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') { if ((<MatrixError>e)?.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
this.requestKeysForEvent(event); this.requestKeysForEvent(event);
errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX'; errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX';
@ -1363,7 +1364,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
const recipients = event.getKeyRequestRecipients(this.userId); const recipients = event.getKeyRequestRecipients(this.userId);
this.crypto.requestRoomKey({ this.crypto.requestRoomKey({
room_id: event.getRoomId(), room_id: event.getRoomId()!,
algorithm: wireContent.algorithm, algorithm: wireContent.algorithm,
sender_key: wireContent.sender_key, sender_key: wireContent.sender_key,
session_id: wireContent.session_id, session_id: wireContent.session_id,
@ -1384,7 +1385,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
if (!this.pendingEvents.has(senderKey)) { if (!this.pendingEvents.has(senderKey)) {
this.pendingEvents.set(senderKey, new Map<string, Set<MatrixEvent>>()); this.pendingEvents.set(senderKey, new Map<string, Set<MatrixEvent>>());
} }
const senderPendingEvents = this.pendingEvents.get(senderKey); const senderPendingEvents = this.pendingEvents.get(senderKey)!;
if (!senderPendingEvents.has(sessionId)) { if (!senderPendingEvents.has(sessionId)) {
senderPendingEvents.set(sessionId, new Set()); senderPendingEvents.set(sessionId, new Set());
} }
@ -1410,9 +1411,9 @@ class MegolmDecryption extends DecryptionAlgorithm {
pendingEvents.delete(event); pendingEvents.delete(event);
if (pendingEvents.size === 0) { if (pendingEvents.size === 0) {
senderPendingEvents.delete(sessionId); senderPendingEvents!.delete(sessionId);
} }
if (senderPendingEvents.size === 0) { if (senderPendingEvents!.size === 0) {
this.pendingEvents.delete(senderKey); this.pendingEvents.delete(senderKey);
} }
} }
@ -1424,7 +1425,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
*/ */
public async onRoomKeyEvent(event: MatrixEvent): Promise<void> { public async onRoomKeyEvent(event: MatrixEvent): Promise<void> {
const content = event.getContent<Partial<IMessage["content"]>>(); const content = event.getContent<Partial<IMessage["content"]>>();
let senderKey = event.getSenderKey(); let senderKey = event.getSenderKey()!;
let forwardingKeyChain: string[] = []; let forwardingKeyChain: string[] = [];
let exportFormat = false; let exportFormat = false;
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
@ -1454,7 +1455,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
olmlib.OLM_ALGORITHM, olmlib.OLM_ALGORITHM,
senderKey, senderKey,
); );
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey( const senderKeyUser = this.baseApis.crypto!.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM, olmlib.OLM_ALGORITHM,
senderKey, senderKey,
); );
@ -1533,13 +1534,16 @@ class MegolmDecryption extends DecryptionAlgorithm {
await this.crypto.cryptoStore.doTxn( await this.crypto.cryptoStore.doTxn(
'readwrite', 'readwrite',
['parked_shared_history'], ['parked_shared_history'],
(txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id, parkedData, txn), (txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id!, parkedData, txn),
logger.withPrefix("[addParkedSharedHistory]"), logger.withPrefix("[addParkedSharedHistory]"),
); );
return; return;
} }
const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey); const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(
olmlib.OLM_ALGORITHM,
senderKey,
) ?? undefined;
const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender(), sendingDevice); const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender(), sendingDevice);
if (fromUs && !deviceTrust.isVerified()) { if (fromUs && !deviceTrust.isVerified()) {
@ -1698,7 +1702,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void { public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void {
const userId = keyRequest.userId; const userId = keyRequest.userId;
const deviceId = keyRequest.deviceId; const deviceId = keyRequest.deviceId;
const deviceInfo = this.crypto.getStoredDevice(userId, deviceId); const deviceInfo = this.crypto.getStoredDevice(userId, deviceId)!;
const body = keyRequest.requestBody; const body = keyRequest.requestBody;
this.olmlib.ensureOlmSessionsForDevices( this.olmlib.ensureOlmSessionsForDevices(
@ -1739,7 +1743,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
this.olmDevice, this.olmDevice,
userId, userId,
deviceInfo, deviceInfo,
payload, payload!,
).then(() => { ).then(() => {
const contentMap = { const contentMap = {
[userId]: { [userId]: {
@ -1766,12 +1770,12 @@ class MegolmDecryption extends DecryptionAlgorithm {
"algorithm": olmlib.MEGOLM_ALGORITHM, "algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": roomId, "room_id": roomId,
"sender_key": senderKey, "sender_key": senderKey,
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key, "sender_claimed_ed25519_key": key!.sender_claimed_ed25519_key!,
"session_id": sessionId, "session_id": sessionId,
"session_key": key.key, "session_key": key!.key,
"chain_index": key.chain_index, "chain_index": key!.chain_index,
"forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain, "forwarding_curve25519_key_chain": key!.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": key.shared_history || false, "org.matrix.msc3061.shared_history": key!.shared_history || false,
}, },
}; };
} }
@ -1901,7 +1905,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
for (const deviceInfo of devices) { for (const deviceInfo of devices) {
const encryptedContent: IEncryptedContent = { const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM, algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key, sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {}, ciphertext: {},
}; };
contentMap[userId][deviceInfo.deviceId] = encryptedContent; contentMap[userId][deviceInfo.deviceId] = encryptedContent;

View File

@ -180,14 +180,14 @@ class OlmDecryption extends DecryptionAlgorithm {
); );
} }
if (!(this.olmDevice.deviceCurve25519Key in ciphertext)) { if (!(this.olmDevice.deviceCurve25519Key! in ciphertext)) {
throw new DecryptionError( throw new DecryptionError(
"OLM_NOT_INCLUDED_IN_RECIPIENTS", "OLM_NOT_INCLUDED_IN_RECIPIENTS",
"Not included in recipients", "Not included in recipients",
); );
} }
const message = ciphertext[this.olmDevice.deviceCurve25519Key]; const message = ciphertext[this.olmDevice.deviceCurve25519Key!];
let payloadString; let payloadString: string;
try { try {
payloadString = await this.decryptMessage(deviceKey, message); payloadString = await this.decryptMessage(deviceKey, message);
@ -196,7 +196,7 @@ class OlmDecryption extends DecryptionAlgorithm {
"OLM_BAD_ENCRYPTED_MESSAGE", "OLM_BAD_ENCRYPTED_MESSAGE",
"Bad Encrypted Message", { "Bad Encrypted Message", {
sender: deviceKey, sender: deviceKey,
err: e, err: e as Error,
}, },
); );
} }
@ -217,7 +217,7 @@ class OlmDecryption extends DecryptionAlgorithm {
"OLM_BAD_RECIPIENT_KEY", "OLM_BAD_RECIPIENT_KEY",
"Message not intended for this device", { "Message not intended for this device", {
intended: payload.recipient_keys.ed25519, intended: payload.recipient_keys.ed25519,
our_key: this.olmDevice.deviceEd25519Key, our_key: this.olmDevice.deviceEd25519Key!,
}, },
); );
} }
@ -233,7 +233,7 @@ class OlmDecryption extends DecryptionAlgorithm {
olmlib.OLM_ALGORITHM, olmlib.OLM_ALGORITHM,
deviceKey, deviceKey,
); );
if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined) { if (senderKeyUser !== event.getSender() && senderKeyUser != undefined) {
throw new DecryptionError( throw new DecryptionError(
"OLM_BAD_SENDER", "OLM_BAD_SENDER",
"Message claimed to be from " + event.getSender(), { "Message claimed to be from " + event.getSender(), {
@ -325,13 +325,13 @@ class OlmDecryption extends DecryptionAlgorithm {
// session, so it should have worked. // session, so it should have worked.
throw new Error( throw new Error(
"Error decrypting prekey message with existing session id " + "Error decrypting prekey message with existing session id " +
sessionId + ": " + e.message, sessionId + ": " + (<Error>e).message,
); );
} }
// otherwise it's probably a message for another session; carry on, but // otherwise it's probably a message for another session; carry on, but
// keep a record of the error // keep a record of the error
decryptionErrors[sessionId] = e.message; decryptionErrors[sessionId] = (<Error>e).message;
} }
} }
@ -358,7 +358,7 @@ class OlmDecryption extends DecryptionAlgorithm {
theirDeviceIdentityKey, message.type, message.body, theirDeviceIdentityKey, message.type, message.body,
); );
} catch (e) { } catch (e) {
decryptionErrors["(new)"] = e.message; decryptionErrors["(new)"] = (<Error>e).message;
throw new Error( throw new Error(
"Error decrypting prekey message: " + "Error decrypting prekey message: " +
JSON.stringify(decryptionErrors), JSON.stringify(decryptionErrors),

View File

@ -40,6 +40,7 @@ import {
import { UnstableValue } from "../NamespacedValue"; import { UnstableValue } from "../NamespacedValue";
import { CryptoEvent, IMegolmSessionData } from "./index"; import { CryptoEvent, IMegolmSessionData } from "./index";
import { crypto } from "./crypto"; import { crypto } from "./crypto";
import { HTTPError, MatrixError } from "../http-api";
const KEY_BACKUP_KEYS_PER_REQUEST = 200; const KEY_BACKUP_KEYS_PER_REQUEST = 200;
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
@ -62,7 +63,7 @@ export type TrustInfo = {
}; };
export interface IKeyBackupCheck { export interface IKeyBackupCheck {
backupInfo: IKeyBackupInfo; backupInfo?: IKeyBackupInfo;
trustInfo: TrustInfo; trustInfo: TrustInfo;
} }
@ -85,9 +86,7 @@ interface BackupAlgorithmClass {
init(authData: AuthData, getKey: GetKey): Promise<BackupAlgorithm>; init(authData: AuthData, getKey: GetKey): Promise<BackupAlgorithm>;
// prepare a brand new backup // prepare a brand new backup
prepare( prepare(key?: string | Uint8Array | null): Promise<[Uint8Array, AuthData]>;
key: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]>;
checkBackupVersion(info: IKeyBackupInfo): void; checkBackupVersion(info: IKeyBackupInfo): void;
} }
@ -221,19 +220,19 @@ export class BackupManager {
* one of the user's verified devices, start backing up * one of the user's verified devices, start backing up
* to it. * to it.
*/ */
public async checkAndStart(): Promise<IKeyBackupCheck> { public async checkAndStart(): Promise<IKeyBackupCheck | null> {
logger.log("Checking key backup status..."); logger.log("Checking key backup status...");
if (this.baseApis.isGuest()) { if (this.baseApis.isGuest()) {
logger.log("Skipping key backup check since user is guest"); logger.log("Skipping key backup check since user is guest");
this.checkedForBackup = true; this.checkedForBackup = true;
return null; return null;
} }
let backupInfo: IKeyBackupInfo; let backupInfo: IKeyBackupInfo | undefined;
try { try {
backupInfo = await this.baseApis.getKeyBackupVersion(); backupInfo = await this.baseApis.getKeyBackupVersion() ?? undefined;
} catch (e) { } catch (e) {
logger.log("Error checking for active key backup", e); logger.log("Error checking for active key backup", e);
if (e.httpStatus === 404) { if ((<HTTPError>e).httpStatus === 404) {
// 404 is returned when the key backup does not exist, so that // 404 is returned when the key backup does not exist, so that
// counts as successfully checking. // counts as successfully checking.
this.checkedForBackup = true; this.checkedForBackup = true;
@ -245,11 +244,8 @@ export class BackupManager {
const trustInfo = await this.isKeyBackupTrusted(backupInfo); const trustInfo = await this.isKeyBackupTrusted(backupInfo);
if (trustInfo.usable && !this.backupInfo) { if (trustInfo.usable && !this.backupInfo) {
logger.log( logger.log(`Found usable key backup v${backupInfo!.version}: enabling key backups`);
"Found usable key backup v" + backupInfo.version + await this.enableKeyBackup(backupInfo!);
": enabling key backups",
);
await this.enableKeyBackup(backupInfo);
} else if (!trustInfo.usable && this.backupInfo) { } else if (!trustInfo.usable && this.backupInfo) {
logger.log("No usable key backup: disabling key backup"); logger.log("No usable key backup: disabling key backup");
this.disableKeyBackup(); this.disableKeyBackup();
@ -257,13 +253,11 @@ export class BackupManager {
logger.log("No usable key backup: not enabling key backup"); logger.log("No usable key backup: not enabling key backup");
} else if (trustInfo.usable && this.backupInfo) { } else if (trustInfo.usable && this.backupInfo) {
// may not be the same version: if not, we should switch // may not be the same version: if not, we should switch
if (backupInfo.version !== this.backupInfo.version) { if (backupInfo!.version !== this.backupInfo.version) {
logger.log( logger.log(`On backup version ${this.backupInfo.version} but ` +
"On backup version " + this.backupInfo.version + " but found " + `found version ${backupInfo!.version}: switching.`);
"version " + backupInfo.version + ": switching.",
);
this.disableKeyBackup(); this.disableKeyBackup();
await this.enableKeyBackup(backupInfo); await this.enableKeyBackup(backupInfo!);
// We're now using a new backup, so schedule all the keys we have to be // We're now using a new backup, so schedule all the keys we have to be
// uploaded to the new backup. This is a bit of a workaround to upload // uploaded to the new backup. This is a bit of a workaround to upload
// keys to a new backup in *most* cases, but it won't cover all cases // keys to a new backup in *most* cases, but it won't cover all cases
@ -271,7 +265,7 @@ export class BackupManager {
// see https://github.com/vector-im/element-web/issues/14833 // see https://github.com/vector-im/element-web/issues/14833
await this.scheduleAllGroupSessionsForBackup(); await this.scheduleAllGroupSessionsForBackup();
} else { } else {
logger.log("Backup version " + backupInfo.version + " still current"); logger.log(`Backup version ${backupInfo!.version} still current`);
} }
} }
@ -287,7 +281,7 @@ export class BackupManager {
* trust information (as returned by isKeyBackupTrusted) * trust information (as returned by isKeyBackupTrusted)
* in trustInfo. * in trustInfo.
*/ */
public async checkKeyBackup(): Promise<IKeyBackupCheck> { public async checkKeyBackup(): Promise<IKeyBackupCheck | null> {
this.checkedForBackup = false; this.checkedForBackup = false;
return this.checkAndStart(); return this.checkAndStart();
} }
@ -325,7 +319,7 @@ export class BackupManager {
* ] * ]
* } * }
*/ */
public async isKeyBackupTrusted(backupInfo: IKeyBackupInfo): Promise<TrustInfo> { public async isKeyBackupTrusted(backupInfo?: IKeyBackupInfo): Promise<TrustInfo> {
const ret = { const ret = {
usable: false, usable: false,
trusted_locally: false, trusted_locally: false,
@ -342,9 +336,10 @@ export class BackupManager {
return ret; return ret;
} }
const privKey = await this.baseApis.crypto.getSessionBackupPrivateKey(); const userId = this.baseApis.getUserId()!;
const privKey = await this.baseApis.crypto!.getSessionBackupPrivateKey();
if (privKey) { if (privKey) {
let algorithm; let algorithm: BackupAlgorithm | null = null;
try { try {
algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => privKey); algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => privKey);
@ -356,13 +351,11 @@ export class BackupManager {
// do nothing -- if we have an error, then we don't mark it as // do nothing -- if we have an error, then we don't mark it as
// locally trusted // locally trusted
} finally { } finally {
if (algorithm) { algorithm?.free();
algorithm.free();
}
} }
} }
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || {}; const mySigs = backupInfo.auth_data.signatures[userId] || {};
for (const keyId of Object.keys(mySigs)) { for (const keyId of Object.keys(mySigs)) {
const keyIdParts = keyId.split(':'); const keyIdParts = keyId.split(':');
@ -375,14 +368,14 @@ export class BackupManager {
const sigInfo: SigInfo = { deviceId: keyIdParts[1] }; const sigInfo: SigInfo = { deviceId: keyIdParts[1] };
// first check to see if it's from our cross-signing key // first check to see if it's from our cross-signing key
const crossSigningId = this.baseApis.crypto.crossSigningInfo.getId(); const crossSigningId = this.baseApis.crypto!.crossSigningInfo.getId();
if (crossSigningId === sigInfo.deviceId) { if (crossSigningId === sigInfo.deviceId) {
sigInfo.crossSigningId = true; sigInfo.crossSigningId = true;
try { try {
await verifySignature( await verifySignature(
this.baseApis.crypto.olmDevice, this.baseApis.crypto!.olmDevice,
backupInfo.auth_data, backupInfo.auth_data,
this.baseApis.getUserId(), userId,
sigInfo.deviceId, sigInfo.deviceId,
crossSigningId, crossSigningId,
); );
@ -400,17 +393,16 @@ export class BackupManager {
// Now look for a sig from a device // Now look for a sig from a device
// At some point this can probably go away and we'll just support // At some point this can probably go away and we'll just support
// it being signed by the cross-signing master key // it being signed by the cross-signing master key
const device = this.baseApis.crypto.deviceList.getStoredDevice( const device = this.baseApis.crypto!.deviceList.getStoredDevice(userId, sigInfo.deviceId,
this.baseApis.getUserId(), sigInfo.deviceId,
); );
if (device) { if (device) {
sigInfo.device = device; sigInfo.device = device;
sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(this.baseApis.getUserId(), sigInfo.deviceId); sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(userId, sigInfo.deviceId);
try { try {
await verifySignature( await verifySignature(
this.baseApis.crypto.olmDevice, this.baseApis.crypto!.olmDevice,
backupInfo.auth_data, backupInfo.auth_data,
this.baseApis.getUserId(), userId,
device.deviceId, device.deviceId,
device.getFingerprint(), device.getFingerprint(),
); );
@ -431,12 +423,7 @@ export class BackupManager {
} }
ret.usable = ret.sigs.some((s) => { ret.usable = ret.sigs.some((s) => {
return ( return s.valid && ((s.device && s.deviceTrust?.isVerified()) || (s.crossSigningId));
s.valid && (
(s.device && s.deviceTrust.isVerified()) ||
(s.crossSigningId)
)
);
}); });
return ret; return ret;
} }
@ -474,17 +461,17 @@ export class BackupManager {
} catch (err) { } catch (err) {
numFailures++; numFailures++;
logger.log("Key backup request failed", err); logger.log("Key backup request failed", err);
if (err.data) { if ((<MatrixError>err).data) {
if ( if (
err.data.errcode == 'M_NOT_FOUND' || (<MatrixError>err).data.errcode == 'M_NOT_FOUND' ||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION' (<MatrixError>err).data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
) { ) {
// Re-check key backup status on error, so we can be // Re-check key backup status on error, so we can be
// sure to present the current situation when asked. // sure to present the current situation when asked.
await this.checkKeyBackup(); await this.checkKeyBackup();
// Backup version has changed or this backup version // Backup version has changed or this backup version
// has been deleted // has been deleted
this.baseApis.crypto.emit(CryptoEvent.KeyBackupFailed, err.data.errcode); this.baseApis.crypto!.emit(CryptoEvent.KeyBackupFailed, (<MatrixError>err).data.errcode!);
throw err; throw err;
} }
} }
@ -507,50 +494,50 @@ export class BackupManager {
* @returns {number} Number of sessions backed up * @returns {number} Number of sessions backed up
*/ */
public async backupPendingKeys(limit: number): Promise<number> { public async backupPendingKeys(limit: number): Promise<number> {
const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit); const sessions = await this.baseApis.crypto!.cryptoStore.getSessionsNeedingBackup(limit);
if (!sessions.length) { if (!sessions.length) {
return 0; return 0;
} }
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); let remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining); this.baseApis.crypto!.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
const rooms: IKeyBackup["rooms"] = {}; const rooms: IKeyBackup["rooms"] = {};
for (const session of sessions) { for (const session of sessions) {
const roomId = session.sessionData.room_id; const roomId = session.sessionData!.room_id;
if (rooms[roomId] === undefined) { if (rooms[roomId] === undefined) {
rooms[roomId] = { sessions: {} }; rooms[roomId] = { sessions: {} };
} }
const sessionData = this.baseApis.crypto.olmDevice.exportInboundGroupSession( const sessionData = this.baseApis.crypto!.olmDevice.exportInboundGroupSession(
session.senderKey, session.sessionId, session.sessionData, session.senderKey, session.sessionId, session.sessionData!,
); );
sessionData.algorithm = MEGOLM_ALGORITHM; sessionData.algorithm = MEGOLM_ALGORITHM;
const forwardedCount = const forwardedCount =
(sessionData.forwarding_curve25519_key_chain || []).length; (sessionData.forwarding_curve25519_key_chain || []).length;
const userId = this.baseApis.crypto.deviceList.getUserByIdentityKey( const userId = this.baseApis.crypto!.deviceList.getUserByIdentityKey(
MEGOLM_ALGORITHM, session.senderKey, MEGOLM_ALGORITHM, session.senderKey,
); );
const device = this.baseApis.crypto.deviceList.getDeviceByIdentityKey( const device = this.baseApis.crypto!.deviceList.getDeviceByIdentityKey(
MEGOLM_ALGORITHM, session.senderKey, MEGOLM_ALGORITHM, session.senderKey,
); ) ?? undefined;
const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified(); const verified = this.baseApis.crypto!.checkDeviceInfoTrust(userId!, device).isVerified();
rooms[roomId]['sessions'][session.sessionId] = { rooms[roomId]['sessions'][session.sessionId] = {
first_message_index: sessionData.first_known_index, first_message_index: sessionData.first_known_index,
forwarded_count: forwardedCount, forwarded_count: forwardedCount,
is_verified: verified, is_verified: verified,
session_data: await this.algorithm.encryptSession(sessionData), session_data: await this.algorithm!.encryptSession(sessionData),
}; };
} }
await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, { rooms }); await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo!.version, { rooms });
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions); await this.baseApis.crypto!.cryptoStore.unmarkSessionsNeedingBackup(sessions);
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining); this.baseApis.crypto!.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
return sessions.length; return sessions.length;
} }
@ -558,7 +545,7 @@ export class BackupManager {
public async backupGroupSession( public async backupGroupSession(
senderKey: string, sessionId: string, senderKey: string, sessionId: string,
): Promise<void> { ): Promise<void> {
await this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([{ await this.baseApis.crypto!.cryptoStore.markSessionsNeedingBackup([{
senderKey: senderKey, senderKey: senderKey,
sessionId: sessionId, sessionId: sessionId,
}]); }]);
@ -590,22 +577,22 @@ export class BackupManager {
* (which will be equal to the number of sessions in the store). * (which will be equal to the number of sessions in the store).
*/ */
public async flagAllGroupSessionsForBackup(): Promise<number> { public async flagAllGroupSessionsForBackup(): Promise<number> {
await this.baseApis.crypto.cryptoStore.doTxn( await this.baseApis.crypto!.cryptoStore.doTxn(
'readwrite', 'readwrite',
[ [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_BACKUP, IndexedDBCryptoStore.STORE_BACKUP,
], ],
(txn) => { (txn) => {
this.baseApis.crypto.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { this.baseApis.crypto!.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
if (session !== null) { if (session !== null) {
this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([session], txn); this.baseApis.crypto!.cryptoStore.markSessionsNeedingBackup([session], txn);
} }
}); });
}, },
); );
const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); const remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
this.baseApis.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining); this.baseApis.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
return remaining; return remaining;
} }
@ -615,7 +602,7 @@ export class BackupManager {
* @returns {Promise<int>} Resolves to the number of sessions requiring backup * @returns {Promise<int>} Resolves to the number of sessions requiring backup
*/ */
public countSessionsNeedingBackup(): Promise<number> { public countSessionsNeedingBackup(): Promise<number> {
return this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); return this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
} }
} }
@ -641,7 +628,7 @@ export class Curve25519 implements BackupAlgorithm {
} }
public static async prepare( public static async prepare(
key: string | Uint8Array | null, key?: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]> { ): Promise<[Uint8Array, AuthData]> {
const decryption = new global.Olm.PkDecryption(); const decryption = new global.Olm.PkDecryption();
try { try {
@ -741,7 +728,10 @@ function randomBytes(size: number): Uint8Array {
return buf; return buf;
} }
const UNSTABLE_MSC3270_NAME = new UnstableValue(null, "org.matrix.msc3270.v1.aes-hmac-sha2"); const UNSTABLE_MSC3270_NAME = new UnstableValue(
"m.megolm_backup.v1.aes-hmac-sha2",
"org.matrix.msc3270.v1.aes-hmac-sha2",
);
export class Aes256 implements BackupAlgorithm { export class Aes256 implements BackupAlgorithm {
public static algorithmName = UNSTABLE_MSC3270_NAME.name; public static algorithmName = UNSTABLE_MSC3270_NAME.name;
@ -769,7 +759,7 @@ export class Aes256 implements BackupAlgorithm {
} }
public static async prepare( public static async prepare(
key: string | Uint8Array | null, key?: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]> { ): Promise<[Uint8Array, AuthData]> {
let outKey: Uint8Array; let outKey: Uint8Array;
const authData: Partial<IAes256AuthData> = {}; const authData: Partial<IAes256AuthData> = {};

View File

@ -58,9 +58,9 @@ const oneweek = 7 * 24 * 60 * 60 * 1000;
export class DehydrationManager { export class DehydrationManager {
private inProgress = false; private inProgress = false;
private timeoutId: any; private timeoutId: any;
private key: Uint8Array; private key?: Uint8Array;
private keyInfo: {[props: string]: any}; private keyInfo?: {[props: string]: any};
private deviceDisplayName: string; private deviceDisplayName?: string;
constructor(private readonly crypto: Crypto) { constructor(private readonly crypto: Crypto) {
this.getDehydrationKeyFromCache(); this.getDehydrationKeyFromCache();
@ -97,7 +97,7 @@ export class DehydrationManager {
/** set the key, and queue periodic dehydration to the server in the background */ /** set the key, and queue periodic dehydration to the server in the background */
public async setKeyAndQueueDehydration( public async setKeyAndQueueDehydration(
key: Uint8Array, keyInfo: {[props: string]: any} = {}, key: Uint8Array, keyInfo: {[props: string]: any} = {},
deviceDisplayName: string = undefined, deviceDisplayName?: string,
): Promise<void> { ): Promise<void> {
const matches = await this.setKey(key, keyInfo, deviceDisplayName); const matches = await this.setKey(key, keyInfo, deviceDisplayName);
if (!matches) { if (!matches) {
@ -108,8 +108,8 @@ export class DehydrationManager {
public async setKey( public async setKey(
key: Uint8Array, keyInfo: {[props: string]: any} = {}, key: Uint8Array, keyInfo: {[props: string]: any} = {},
deviceDisplayName: string = undefined, deviceDisplayName?: string,
): Promise<boolean> { ): Promise<boolean | undefined> {
if (!key) { if (!key) {
// unsetting the key -- cancel any pending dehydration task // unsetting the key -- cancel any pending dehydration task
if (this.timeoutId) { if (this.timeoutId) {
@ -135,9 +135,9 @@ export class DehydrationManager {
// dehydrate a new device. If it's the same, we can keep the same // dehydrate a new device. If it's the same, we can keep the same
// device. (Assume that keyInfo and deviceDisplayName will be the // device. (Assume that keyInfo and deviceDisplayName will be the
// same if the key is the same.) // same if the key is the same.)
let matches: boolean = this.key && key.length == this.key.length; let matches: boolean = !!this.key && key.length == this.key.length;
for (let i = 0; matches && i < key.length; i++) { for (let i = 0; matches && i < key.length; i++) {
if (key[i] != this.key[i]) { if (key[i] != this.key![i]) {
matches = false; matches = false;
} }
} }
@ -150,7 +150,7 @@ export class DehydrationManager {
} }
/** returns the device id of the newly created dehydrated device */ /** returns the device id of the newly created dehydrated device */
public async dehydrateDevice(): Promise<string> { public async dehydrateDevice(): Promise<string | undefined> {
if (this.inProgress) { if (this.inProgress) {
logger.log("Dehydration already in progress -- not starting new dehydration"); logger.log("Dehydration already in progress -- not starting new dehydration");
return; return;
@ -164,7 +164,7 @@ export class DehydrationManager {
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey); const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
// update the crypto store with the timestamp // update the crypto store with the timestamp
const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM); const key = await encryptAES(encodeBase64(this.key!), pickleKey, DEHYDRATION_ALGORITHM);
await this.crypto.cryptoStore.doTxn( await this.crypto.cryptoStore.doTxn(
'readwrite', 'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT], [IndexedDBCryptoStore.STORE_ACCOUNT],
@ -174,7 +174,7 @@ export class DehydrationManager {
{ {
keyInfo: this.keyInfo, keyInfo: this.keyInfo,
key, key,
deviceDisplayName: this.deviceDisplayName, deviceDisplayName: this.deviceDisplayName!,
time: Date.now(), time: Date.now(),
}, },
); );
@ -197,14 +197,14 @@ export class DehydrationManager {
account.mark_keys_as_published(); account.mark_keys_as_published();
// dehydrate the account and store it on the server // dehydrate the account and store it on the server
const pickledAccount = account.pickle(new Uint8Array(this.key)); const pickledAccount = account.pickle(new Uint8Array(this.key!));
const deviceData: {[props: string]: any} = { const deviceData: {[props: string]: any} = {
algorithm: DEHYDRATION_ALGORITHM, algorithm: DEHYDRATION_ALGORITHM,
account: pickledAccount, account: pickledAccount,
}; };
if (this.keyInfo.passphrase) { if (this.keyInfo!.passphrase) {
deviceData.passphrase = this.keyInfo.passphrase; deviceData.passphrase = this.keyInfo!.passphrase;
} }
logger.log("Uploading account to server"); logger.log("Uploading account to server");

View File

@ -23,6 +23,7 @@ limitations under the License.
import anotherjson from "another-json"; import anotherjson from "another-json";
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
import { EventType } from "../@types/event"; import { EventType } from "../@types/event";
import { TypedReEmitter } from '../ReEmitter'; import { TypedReEmitter } from '../ReEmitter';
import { logger } from '../logger'; import { logger } from '../logger';
@ -274,7 +275,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
private trustCrossSignedDevices = true; private trustCrossSignedDevices = true;
// the last time we did a check for the number of one-time-keys on the server. // the last time we did a check for the number of one-time-keys on the server.
private lastOneTimeKeyCheck: number = null; private lastOneTimeKeyCheck: number | null = null;
private oneTimeKeyCheckInProgress = false; private oneTimeKeyCheckInProgress = false;
// EncryptionAlgorithm instance for each room // EncryptionAlgorithm instance for each room
@ -317,8 +318,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// processing the response. // processing the response.
private sendKeyRequestsImmediately = false; private sendKeyRequestsImmediately = false;
private oneTimeKeyCount: number; private oneTimeKeyCount?: number;
private needsNewFallback: boolean; private needsNewFallback?: boolean;
private fallbackCleanup?: ReturnType<typeof setTimeout>; private fallbackCleanup?: ReturnType<typeof setTimeout>;
/** /**
@ -399,8 +400,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// store the fixed version // store the fixed version
const fixedKey = fixBackupKey(storedKey); const fixedKey = fixBackupKey(storedKey);
if (fixedKey) { if (fixedKey) {
const [keyId] = await this.getSecretStorageKey(); const keys = await this.getSecretStorageKey();
await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]); await this.storeSecret("m.megolm_backup.v1", fixedKey, [keys![0]]);
} }
return olmlib.decodeBase64(fixedKey || storedKey); return olmlib.decodeBase64(fixedKey || storedKey);
@ -468,8 +469,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
await this.deviceList.load(); await this.deviceList.load();
// build our device keys: these will later be uploaded // build our device keys: these will later be uploaded
this.deviceKeys["ed25519:" + this.deviceId] = this.olmDevice.deviceEd25519Key; this.deviceKeys["ed25519:" + this.deviceId] = this.olmDevice.deviceEd25519Key!;
this.deviceKeys["curve25519:" + this.deviceId] = this.olmDevice.deviceCurve25519Key; this.deviceKeys["curve25519:" + this.deviceId] = this.olmDevice.deviceCurve25519Key!;
logger.log("Crypto: fetching own devices..."); logger.log("Crypto: fetching own devices...");
let myDevices = this.deviceList.getRawStoredDevicesForUser(this.userId); let myDevices = this.deviceList.getRawStoredDevicesForUser(this.userId);
@ -547,7 +548,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
!deviceTrust.isLocallyVerified() && !deviceTrust.isLocallyVerified() &&
deviceTrust.isCrossSigningVerified() deviceTrust.isCrossSigningVerified()
) { ) {
const deviceObj = this.deviceList.getStoredDevice(userId, deviceId); const deviceObj = this.deviceList.getStoredDevice(userId, deviceId)!;
this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj); this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
} }
} }
@ -695,9 +696,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys); builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys);
// Cross-sign own device // Cross-sign own device
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId); const device = this.deviceList.getStoredDevice(this.userId, this.deviceId)!;
const deviceSignature = await crossSigningInfo.signDevice(this.userId, device); const deviceSignature = await crossSigningInfo.signDevice(this.userId, device);
builder.addKeySignature(this.userId, this.deviceId, deviceSignature); builder.addKeySignature(this.userId, this.deviceId, deviceSignature!);
// Sign message key backup with cross-signing master key // Sign message key backup with cross-signing master key
if (this.backupManager.backupInfo) { if (this.backupManager.backupInfo) {
@ -838,10 +839,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
); );
// the ID of the new SSSS key, if we create one // the ID of the new SSSS key, if we create one
let newKeyId = null; let newKeyId: string | null = null;
// create a new SSSS key and set it as default // create a new SSSS key and set it as default
const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey: Uint8Array) => { const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey?: Uint8Array) => {
if (privateKey) { if (privateKey) {
opts.key = privateKey; opts.key = privateKey;
} }
@ -859,7 +860,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: ISecretStorageKeyInfo) => { const ensureCanCheckPassphrase = async (keyId: string, keyInfo: ISecretStorageKeyInfo) => {
if (!keyInfo.mac) { if (!keyInfo.mac) {
const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey( const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey?.(
{ keys: { [keyId]: keyInfo } }, "", { keys: { [keyId]: keyInfo } }, "",
); );
if (key) { if (key) {
@ -934,7 +935,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// if we have the backup key already cached, use it; otherwise use the // if we have the backup key already cached, use it; otherwise use the
// callback to prompt for the key // callback to prompt for the key
const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase(); const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase?.();
// create a new SSSS key and use the backup key as the new SSSS key // create a new SSSS key and use the backup key as the new SSSS key
const opts = {} as IAddSecretStorageKeyOpts; const opts = {} as IAddSecretStorageKeyOpts;
@ -955,7 +956,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
newKeyId = await createSSSS(opts, backupKey); newKeyId = await createSSSS(opts, backupKey);
// store the backup key in secret storage // store the backup key in secret storage
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId]); await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey!), [newKeyId]);
// The backup is trusted because the user provided the private key. // The backup is trusted because the user provided the private key.
// Sign the backup with the cross-signing key so the key backup can // Sign the backup with the cross-signing key so the key backup can
@ -1025,8 +1026,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// in secret storage // in secret storage
const fixedBackupKey = fixBackupKey(sessionBackupKey); const fixedBackupKey = fixBackupKey(sessionBackupKey);
if (fixedBackupKey) { if (fixedBackupKey) {
const keyId = newKeyId || oldKeyId;
await secretStorage.store("m.megolm_backup.v1", await secretStorage.store("m.megolm_backup.v1",
fixedBackupKey, [newKeyId || oldKeyId], fixedBackupKey, keyId ? [keyId] : null,
); );
} }
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64( const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
@ -1036,7 +1038,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
} else if (this.backupManager.getKeyBackupEnabled()) { } else if (this.backupManager.getKeyBackupEnabled()) {
// key backup is enabled but we don't have a session backup key in SSSS: see if we have one in // key backup is enabled but we don't have a session backup key in SSSS: see if we have one in
// the cache or the user can provide one, and if so, write it to SSSS // the cache or the user can provide one, and if so, write it to SSSS
const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase(); const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase?.();
if (!backupKey) { if (!backupKey) {
// This will require user intervention to recover from since we don't have the key // This will require user intervention to recover from since we don't have the key
// backup key anywhere. The user should probably just set up a new key backup and // backup key anywhere. The user should probably just set up a new key backup and
@ -1061,16 +1063,16 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
public addSecretStorageKey( public addSecretStorageKey(
algorithm: string, algorithm: string,
opts: IAddSecretStorageKeyOpts, opts: IAddSecretStorageKeyOpts,
keyID: string, keyID?: string,
): Promise<SecretStorageKeyObject> { ): Promise<SecretStorageKeyObject> {
return this.secretStorage.addKey(algorithm, opts, keyID); return this.secretStorage.addKey(algorithm, opts, keyID);
} }
public hasSecretStorageKey(keyID: string): Promise<boolean> { public hasSecretStorageKey(keyID?: string): Promise<boolean> {
return this.secretStorage.hasKey(keyID); return this.secretStorage.hasKey(keyID);
} }
public getSecretStorageKey(keyID?: string): Promise<SecretStorageKeyTuple> { public getSecretStorageKey(keyID?: string): Promise<SecretStorageKeyTuple | null> {
return this.secretStorage.getKey(keyID); return this.secretStorage.getKey(keyID);
} }
@ -1078,7 +1080,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return this.secretStorage.store(name, secret, keys); return this.secretStorage.store(name, secret, keys);
} }
public getSecret(name: string): Promise<string> { public getSecret(name: string): Promise<string | undefined> {
return this.secretStorage.get(name); return this.secretStorage.get(name);
} }
@ -1115,14 +1117,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @returns {boolean} true if the key matches, otherwise false * @returns {boolean} true if the key matches, otherwise false
*/ */
public checkSecretStoragePrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean { public checkSecretStoragePrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
let decryption = null; let decryption: PkDecryption | null = null;
try { try {
decryption = new global.Olm.PkDecryption(); decryption = new global.Olm.PkDecryption();
const gotPubkey = decryption.init_with_private_key(privateKey); const gotPubkey = decryption.init_with_private_key(privateKey);
// make sure it agrees with the given pubkey // make sure it agrees with the given pubkey
return gotPubkey === expectedPublicKey; return gotPubkey === expectedPublicKey;
} finally { } finally {
if (decryption) decryption.free(); decryption?.free();
} }
} }
@ -1188,14 +1190,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @returns {boolean} true if the key matches, otherwise false * @returns {boolean} true if the key matches, otherwise false
*/ */
public checkCrossSigningPrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean { public checkCrossSigningPrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
let signing = null; let signing: PkSigning | null = null;
try { try {
signing = new global.Olm.PkSigning(); signing = new global.Olm.PkSigning();
const gotPubkey = signing.init_with_seed(privateKey); const gotPubkey = signing.init_with_seed(privateKey);
// make sure it agrees with the given pubkey // make sure it agrees with the given pubkey
return gotPubkey === expectedPublicKey; return gotPubkey === expectedPublicKey;
} finally { } finally {
if (signing) signing.free(); signing?.free();
} }
} }
@ -1209,14 +1211,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
logger.info("Starting cross-signing key change post-processing"); logger.info("Starting cross-signing key change post-processing");
// sign the current device with the new key, and upload to the server // sign the current device with the new key, and upload to the server
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId); const device = this.deviceList.getStoredDevice(this.userId, this.deviceId)!;
const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device); const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
logger.info(`Starting background key sig upload for ${this.deviceId}`); logger.info(`Starting background key sig upload for ${this.deviceId}`);
const upload = ({ shouldEmit = false }) => { const upload = ({ shouldEmit = false }) => {
return this.baseApis.uploadKeySignatures({ return this.baseApis.uploadKeySignatures({
[this.userId]: { [this.userId]: {
[this.deviceId]: signedDevice, [this.deviceId]: signedDevice!,
}, },
}).then((response) => { }).then((response) => {
const { failures } = response || {}; const { failures } = response || {};
@ -1267,9 +1269,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
if (usersToUpgrade) { if (usersToUpgrade) {
for (const userId of usersToUpgrade) { for (const userId of usersToUpgrade) {
if (userId in users) { if (userId in users) {
await this.baseApis.setDeviceVerified( await this.baseApis.setDeviceVerified(userId, users[userId].crossSigningInfo.getId()!);
userId, users[userId].crossSigningInfo.getId(),
);
} }
} }
} }
@ -1296,7 +1296,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
private async checkForDeviceVerificationUpgrade( private async checkForDeviceVerificationUpgrade(
userId: string, userId: string,
crossSigningInfo: CrossSigningInfo, crossSigningInfo: CrossSigningInfo,
): Promise<IDeviceVerificationUpgrade> { ): Promise<IDeviceVerificationUpgrade | undefined> {
// only upgrade if this is the first cross-signing key that we've seen for // only upgrade if this is the first cross-signing key that we've seen for
// them, and if their cross-signing key isn't already verified // them, and if their cross-signing key isn't already verified
const trustLevel = this.crossSigningInfo.checkUserTrust(crossSigningInfo); const trustLevel = this.crossSigningInfo.checkUserTrust(crossSigningInfo);
@ -1359,7 +1359,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* *
* @returns {string} the key ID * @returns {string} the key ID
*/ */
public getCrossSigningId(type: string): string { public getCrossSigningId(type: string): string | null {
return this.crossSigningInfo.getId(type); return this.crossSigningInfo.getId(type);
} }
@ -1370,7 +1370,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* *
* @returns {CrossSigningInfo} the cross signing information for the user. * @returns {CrossSigningInfo} the cross signing information for the user.
*/ */
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo { public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
return this.deviceList.getStoredCrossSigningForUser(userId); return this.deviceList.getStoredCrossSigningForUser(userId);
} }
@ -1410,8 +1410,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* *
* @returns {DeviceTrustLevel} * @returns {DeviceTrustLevel}
*/ */
public checkDeviceInfoTrust(userId: string, device: DeviceInfo): DeviceTrustLevel { public checkDeviceInfoTrust(userId: string, device?: DeviceInfo): DeviceTrustLevel {
const trustedLocally = !!(device && device.isVerified()); const trustedLocally = !!device?.isVerified();
const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId); const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
if (device && userCrossSigning) { if (device && userCrossSigning) {
@ -1436,13 +1436,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*/ */
public checkIfOwnDeviceCrossSigned(deviceId: string): boolean { public checkIfOwnDeviceCrossSigned(deviceId: string): boolean {
const device = this.deviceList.getStoredDevice(this.userId, deviceId); const device = this.deviceList.getStoredDevice(this.userId, deviceId);
if (!device) return false;
const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(this.userId); const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(this.userId);
return userCrossSigning.checkDeviceTrust( return userCrossSigning?.checkDeviceTrust(
userCrossSigning, userCrossSigning,
device, device,
false, false,
true, true,
).isCrossSigningVerified(); ).isCrossSigningVerified() ?? false;
} }
/* /*
@ -1494,7 +1495,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* Check the copy of our cross-signing key that we have in the device list and * Check the copy of our cross-signing key that we have in the device list and
* see if we can get the private key. If so, mark it as trusted. * see if we can get the private key. If so, mark it as trusted.
*/ */
async checkOwnCrossSigningTrust({ public async checkOwnCrossSigningTrust({
allowPrivateKeyRequests = false, allowPrivateKeyRequests = false,
}: ICheckOwnCrossSigningTrustOpts = {}): Promise<void> { }: ICheckOwnCrossSigningTrustOpts = {}): Promise<void> {
const userId = this.userId; const userId = this.userId;
@ -1520,7 +1521,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return; return;
} }
const seenPubkey = newCrossSigning.getId(); const seenPubkey = newCrossSigning.getId()!;
const masterChanged = this.crossSigningInfo.getId() !== seenPubkey; const masterChanged = this.crossSigningInfo.getId() !== seenPubkey;
const masterExistsNotLocallyCached = const masterExistsNotLocallyCached =
newCrossSigning.getId() && !crossSigningPrivateKeys.has("master"); newCrossSigning.getId() && !crossSigningPrivateKeys.has("master");
@ -1532,18 +1533,16 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
(masterChanged || masterExistsNotLocallyCached) (masterChanged || masterExistsNotLocallyCached)
) { ) {
logger.info("Attempting to retrieve cross-signing master private key"); logger.info("Attempting to retrieve cross-signing master private key");
let signing = null; let signing: PkSigning | null = null;
// It's important for control flow that we leave any errors alone for // It's important for control flow that we leave any errors alone for
// higher levels to handle so that e.g. cancelling access properly // higher levels to handle so that e.g. cancelling access properly
// aborts any larger operation as well. // aborts any larger operation as well.
try { try {
const ret = await this.crossSigningInfo.getCrossSigningKey( const ret = await this.crossSigningInfo.getCrossSigningKey('master', seenPubkey);
'master', seenPubkey,
);
signing = ret[1]; signing = ret[1];
logger.info("Got cross-signing master private key"); logger.info("Got cross-signing master private key");
} finally { } finally {
if (signing) signing.free(); signing?.free();
} }
} }
@ -1575,22 +1574,20 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
(selfSigningChanged || selfSigningExistsNotLocallyCached) (selfSigningChanged || selfSigningExistsNotLocallyCached)
) { ) {
logger.info("Attempting to retrieve cross-signing self-signing private key"); logger.info("Attempting to retrieve cross-signing self-signing private key");
let signing = null; let signing: PkSigning | null = null;
try { try {
const ret = await this.crossSigningInfo.getCrossSigningKey( const ret = await this.crossSigningInfo.getCrossSigningKey(
"self_signing", newCrossSigning.getId("self_signing"), "self_signing", newCrossSigning.getId("self_signing")!,
); );
signing = ret[1]; signing = ret[1];
logger.info("Got cross-signing self-signing private key"); logger.info("Got cross-signing self-signing private key");
} finally { } finally {
if (signing) signing.free(); signing?.free();
} }
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId); const device = this.deviceList.getStoredDevice(this.userId, this.deviceId)!;
const signedDevice = await this.crossSigningInfo.signDevice( const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
this.userId, device, keySignatures[this.deviceId] = signedDevice!;
);
keySignatures[this.deviceId] = signedDevice;
} }
if (userSigningChanged) { if (userSigningChanged) {
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing")); logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
@ -1600,26 +1597,26 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
(userSigningChanged || userSigningExistsNotLocallyCached) (userSigningChanged || userSigningExistsNotLocallyCached)
) { ) {
logger.info("Attempting to retrieve cross-signing user-signing private key"); logger.info("Attempting to retrieve cross-signing user-signing private key");
let signing = null; let signing: PkSigning | null = null;
try { try {
const ret = await this.crossSigningInfo.getCrossSigningKey( const ret = await this.crossSigningInfo.getCrossSigningKey(
"user_signing", newCrossSigning.getId("user_signing"), "user_signing", newCrossSigning.getId("user_signing")!,
); );
signing = ret[1]; signing = ret[1];
logger.info("Got cross-signing user-signing private key"); logger.info("Got cross-signing user-signing private key");
} finally { } finally {
if (signing) signing.free(); signing?.free();
} }
} }
if (masterChanged) { if (masterChanged) {
const masterKey = this.crossSigningInfo.keys.master; const masterKey = this.crossSigningInfo.keys.master;
await this.signObject(masterKey); await this.signObject(masterKey);
const deviceSig = masterKey.signatures[this.userId]["ed25519:" + this.deviceId]; const deviceSig = masterKey.signatures![this.userId]["ed25519:" + this.deviceId];
// Include only the _new_ device signature in the upload. // Include only the _new_ device signature in the upload.
// We may have existing signatures from deleted devices, which will cause // We may have existing signatures from deleted devices, which will cause
// the entire upload to fail. // the entire upload to fail.
keySignatures[this.crossSigningInfo.getId()] = Object.assign( keySignatures[this.crossSigningInfo.getId()!] = Object.assign(
{} as ISignedKey, {} as ISignedKey,
masterKey, masterKey,
{ {
@ -1679,7 +1676,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* *
* @param {object} keys The new trusted set of keys * @param {object} keys The new trusted set of keys
*/ */
private async storeTrustedSelfKeys(keys: Record<string, ICrossSigningKey>): Promise<void> { private async storeTrustedSelfKeys(keys: Record<string, ICrossSigningKey> | null): Promise<void> {
if (keys) { if (keys) {
this.crossSigningInfo.setKeys(keys); this.crossSigningInfo.setKeys(keys);
} else { } else {
@ -1721,9 +1718,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}, },
}); });
if (usersToUpgrade.includes(userId)) { if (usersToUpgrade.includes(userId)) {
await this.baseApis.setDeviceVerified( await this.baseApis.setDeviceVerified(userId, crossSigningInfo.getId()!);
userId, crossSigningInfo.getId(),
);
} }
} }
} }
@ -1771,7 +1766,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* *
* @return {string} base64-encoded ed25519 key. * @return {string} base64-encoded ed25519 key.
*/ */
public getDeviceEd25519Key(): string { public getDeviceEd25519Key(): string | null {
return this.olmDevice.deviceEd25519Key; return this.olmDevice.deviceEd25519Key;
} }
@ -1780,7 +1775,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* *
* @return {string} base64-encoded curve25519 key. * @return {string} base64-encoded curve25519 key.
*/ */
public getDeviceCurve25519Key(): string { public getDeviceCurve25519Key(): string | null {
return this.olmDevice.deviceCurve25519Key; return this.olmDevice.deviceCurve25519Key;
} }
@ -1859,11 +1854,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
} }
public setNeedsNewFallback(needsNewFallback: boolean) { public setNeedsNewFallback(needsNewFallback: boolean) {
this.needsNewFallback = !!needsNewFallback; this.needsNewFallback = needsNewFallback;
} }
public getNeedsNewFallback(): boolean { public getNeedsNewFallback(): boolean {
return this.needsNewFallback; return !!this.needsNewFallback;
} }
// check if it's time to upload one-time keys, and do so if so. // check if it's time to upload one-time keys, and do so if so.
@ -1983,10 +1978,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
} }
// returns a promise which resolves to the response // returns a promise which resolves to the response
private async uploadOneTimeKeys() { private async uploadOneTimeKeys(): Promise<IKeysUploadResponse> {
const promises = []; const promises: Promise<unknown>[] = [];
let fallbackJson: Record<string, IOneTimeKey>; let fallbackJson: Record<string, IOneTimeKey> | undefined;
if (this.getNeedsNewFallback()) { if (this.getNeedsNewFallback()) {
fallbackJson = {}; fallbackJson = {};
const fallbackKeys = await this.olmDevice.getFallbackKey(); const fallbackKeys = await this.olmDevice.getFallbackKey();
@ -2045,7 +2040,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* module:crypto/deviceinfo|DeviceInfo}. * module:crypto/deviceinfo|DeviceInfo}.
*/ */
public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> { public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> {
return this.deviceList.downloadKeys(userIds, forceDownload); return this.deviceList.downloadKeys(userIds, !!forceDownload);
} }
/** /**
@ -2114,17 +2109,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
public async setDeviceVerification( public async setDeviceVerification(
userId: string, userId: string,
deviceId: string, deviceId: string,
verified?: boolean, verified: boolean | null = null,
blocked?: boolean, blocked: boolean | null = null,
known?: boolean, known: boolean | null = null,
keys?: Record<string, string>, keys?: Record<string, string>,
): Promise<DeviceInfo | CrossSigningInfo> { ): Promise<DeviceInfo | CrossSigningInfo> {
// get rid of any `undefined`s here so we can just check
// for null rather than null or undefined
if (verified === undefined) verified = null;
if (blocked === undefined) blocked = null;
if (known === undefined) known = null;
// Check if the 'device' is actually a cross signing key // Check if the 'device' is actually a cross signing key
// The js-sdk's verification treats cross-signing keys as devices // The js-sdk's verification treats cross-signing keys as devices
// and so uses this method to mark them verified. // and so uses this method to mark them verified.
@ -2240,9 +2229,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
if (deviceTrust.isCrossSigningVerified()) { if (deviceTrust.isCrossSigningVerified()) {
logger.log(`Own device ${deviceId} already cross-signing verified`); logger.log(`Own device ${deviceId} already cross-signing verified`);
} else { } else {
device = await this.crossSigningInfo.signDevice( device = (await this.crossSigningInfo.signDevice(
userId, DeviceInfo.fromStorage(dev, deviceId), userId, DeviceInfo.fromStorage(dev, deviceId),
); ))!;
} }
if (device) { if (device) {
@ -2293,7 +2282,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests); return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests);
} }
public requestVerification(userId: string, devices: string[]): Promise<VerificationRequest> { public requestVerification(userId: string, devices?: string[]): Promise<VerificationRequest> {
if (!devices) { if (!devices) {
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId)); devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId));
} }
@ -2597,7 +2586,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// because it first stores in memory. We should await the promise only // because it first stores in memory. We should await the promise only
// after all the in-memory state (roomEncryptors and _roomList) has been updated // after all the in-memory state (roomEncryptors and _roomList) has been updated
// to avoid races when calling this method multiple times. Hence keep a hold of the promise. // to avoid races when calling this method multiple times. Hence keep a hold of the promise.
let storeConfigPromise: Promise<void> = null; let storeConfigPromise: Promise<void> | null = null;
if (!existingConfig) { if (!existingConfig) {
storeConfigPromise = this.roomList.setRoomEncryption(roomId, config); storeConfigPromise = this.roomList.setRoomEncryption(roomId, config);
} }
@ -2754,7 +2743,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const total = keys.length; const total = keys.length;
function updateProgress() { function updateProgress() {
opts.progressCallback({ opts.progressCallback?.({
stage: "load_keys", stage: "load_keys",
successes, successes,
failures, failures,
@ -2809,7 +2798,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @return {Promise?} Promise which resolves when the event has been * @return {Promise?} Promise which resolves when the event has been
* encrypted, or null if nothing was needed * encrypted, or null if nothing was needed
*/ */
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> { public async encryptEvent(event: MatrixEvent, room?: Room): Promise<void> {
if (!room) { if (!room) {
throw new Error("Cannot send encrypted messages in unknown rooms"); throw new Error("Cannot send encrypted messages in unknown rooms");
} }
@ -2862,8 +2851,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
event.makeEncrypted( event.makeEncrypted(
"m.room.encrypted", "m.room.encrypted",
encryptedContent, encryptedContent,
this.olmDevice.deviceCurve25519Key, this.olmDevice.deviceCurve25519Key!,
this.olmDevice.deviceEd25519Key, this.olmDevice.deviceEd25519Key!,
); );
} }
@ -3143,7 +3132,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const deviceId = deviceInfo.deviceId; const deviceId = deviceInfo.deviceId;
const encryptedContent: IEncryptedContent = { const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM, algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key, sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {}, ciphertext: {},
}; };
@ -3753,8 +3742,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* unknown * unknown
*/ */
public getRoomDecryptor(roomId: string, algorithm: string): DecryptionAlgorithm { public getRoomDecryptor(roomId: string, algorithm: string): DecryptionAlgorithm {
let decryptors: Map<string, DecryptionAlgorithm>; let decryptors: Map<string, DecryptionAlgorithm> | undefined;
let alg: DecryptionAlgorithm; let alg: DecryptionAlgorithm | undefined;
roomId = roomId || null; roomId = roomId || null;
if (roomId) { if (roomId) {
@ -3799,10 +3788,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @return {array} An array of room decryptors * @return {array} An array of room decryptors
*/ */
private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] { private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] {
const decryptors = []; const decryptors: DecryptionAlgorithm[] = [];
for (const d of this.roomDecryptors.values()) { for (const d of this.roomDecryptors.values()) {
if (d.has(algorithm)) { if (d.has(algorithm)) {
decryptors.push(d.get(algorithm)); decryptors.push(d.get(algorithm)!);
} }
} }
return decryptors; return decryptors;
@ -3839,7 +3828,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* key will be returned. Otherwise null will be returned. * key will be returned. Otherwise null will be returned.
* *
*/ */
export function fixBackupKey(key: string): string | null { export function fixBackupKey(key?: string): string | null {
if (typeof key !== "string" || key.indexOf(",") < 0) { if (typeof key !== "string" || key.indexOf(",") < 0) {
return null; return null;
} }

View File

@ -21,7 +21,6 @@ limitations under the License.
*/ */
import anotherjson from "another-json"; import anotherjson from "another-json";
import { Logger } from "loglevel";
import type { PkSigning } from "@matrix-org/olm"; import type { PkSigning } from "@matrix-org/olm";
import { OlmDevice } from "./OlmDevice"; import { OlmDevice } from "./OlmDevice";
@ -56,7 +55,7 @@ export const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup;
export interface IOlmSessionResult { export interface IOlmSessionResult {
device: DeviceInfo; device: DeviceInfo;
sessionId?: string; sessionId: string | null;
} }
/** /**
@ -137,7 +136,7 @@ export async function encryptMessageForDevice(
interface IExistingOlmSession { interface IExistingOlmSession {
device: DeviceInfo; device: DeviceInfo;
sessionId?: string; sessionId: string | null;
} }
/** /**
@ -225,19 +224,8 @@ export async function ensureOlmSessionsForDevices(
force = false, force = false,
otkTimeout?: number, otkTimeout?: number,
failedServers?: string[], failedServers?: string[],
log: Logger = logger, log = logger,
): Promise<Record<string, Record<string, IOlmSessionResult>>> { ): Promise<Record<string, Record<string, IOlmSessionResult>>> {
if (typeof force === "number") {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - backwards compatibility
log = failedServers;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - backwards compatibility
failedServers = otkTimeout;
otkTimeout = force;
force = false;
}
const devicesWithoutSession: [string, string][] = [ const devicesWithoutSession: [string, string][] = [
// [userId, deviceId], ... // [userId, deviceId], ...
]; ];
@ -365,7 +353,7 @@ export async function ensureOlmSessionsForDevices(
} }
const deviceRes = userRes[deviceId] || {}; const deviceRes = userRes[deviceId] || {};
let oneTimeKey: IOneTimeKey = null; let oneTimeKey: IOneTimeKey | null = null;
for (const keyId in deviceRes) { for (const keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) { if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId]; oneTimeKey = deviceRes[keyId];
@ -388,7 +376,7 @@ export async function ensureOlmSessionsForDevices(
olmDevice, oneTimeKey, userId, deviceInfo, olmDevice, oneTimeKey, userId, deviceInfo,
).then((sid) => { ).then((sid) => {
if (resolveSession[key]) { if (resolveSession[key]) {
resolveSession[key](sid); resolveSession[key](sid ?? undefined);
} }
result[userId][deviceId].sessionId = sid; result[userId][deviceId].sessionId = sid;
}, (e) => { }, (e) => {
@ -413,7 +401,7 @@ async function _verifyKeyAndStartSession(
oneTimeKey: IOneTimeKey, oneTimeKey: IOneTimeKey,
userId: string, userId: string,
deviceInfo: DeviceInfo, deviceInfo: DeviceInfo,
): Promise<string> { ): Promise<string | null> {
const deviceId = deviceInfo.deviceId; const deviceId = deviceInfo.deviceId;
try { try {
await verifySignature( await verifySignature(

View File

@ -90,14 +90,14 @@ export interface CryptoStore {
deviceKey: string, deviceKey: string,
sessionId: string, sessionId: string,
txn: unknown, txn: unknown,
func: (session: ISessionInfo) => void, func: (session: ISessionInfo | null) => void,
): void; ): void;
getEndToEndSessions( getEndToEndSessions(
deviceKey: string, deviceKey: string,
txn: unknown, txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void, func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void; ): void;
getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void; getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo | null) => void): void;
storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void; storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void;
storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void>; storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void>;
getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null>; getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null>;

View File

@ -307,7 +307,7 @@ export class Backend implements CryptoStore {
expectedState: number, expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>, updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> { ): Promise<OutgoingRoomKeyRequest | null> {
let result: OutgoingRoomKeyRequest = null; let result: OutgoingRoomKeyRequest | null = null;
function onsuccess(this: IDBRequest<IDBCursorWithValue>) { function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
const cursor = this.result; const cursor = this.result;
@ -375,7 +375,7 @@ export class Backend implements CryptoStore {
try { try {
func(getReq.result || null); func(getReq.result || null);
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
}; };
} }
@ -395,7 +395,7 @@ export class Backend implements CryptoStore {
try { try {
func(getReq.result || null); func(getReq.result || null);
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
}; };
} }
@ -411,7 +411,7 @@ export class Backend implements CryptoStore {
try { try {
func(getReq.result || null); func(getReq.result || null);
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
}; };
} }
@ -439,7 +439,7 @@ export class Backend implements CryptoStore {
try { try {
func(countReq.result); func(countReq.result);
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
}; };
} }
@ -465,7 +465,7 @@ export class Backend implements CryptoStore {
try { try {
func(results); func(results);
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
} }
}; };
@ -475,7 +475,7 @@ export class Backend implements CryptoStore {
deviceKey: string, deviceKey: string,
sessionId: string, sessionId: string,
txn: IDBTransaction, txn: IDBTransaction,
func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void, func: (session: ISessionInfo | null) => void,
): void { ): void {
const objectStore = txn.objectStore("sessions"); const objectStore = txn.objectStore("sessions");
const getReq = objectStore.get([deviceKey, sessionId]); const getReq = objectStore.get([deviceKey, sessionId]);
@ -490,12 +490,12 @@ export class Backend implements CryptoStore {
func(null); func(null);
} }
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
}; };
} }
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void { public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
const objectStore = txn.objectStore("sessions"); const objectStore = txn.objectStore("sessions");
const getReq = objectStore.openCursor(); const getReq = objectStore.openCursor();
getReq.onsuccess = function() { getReq.onsuccess = function() {
@ -508,7 +508,7 @@ export class Backend implements CryptoStore {
func(null); func(null);
} }
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
}; };
} }
@ -537,11 +537,11 @@ export class Backend implements CryptoStore {
fixed, fixed,
time: Date.now(), time: Date.now(),
}); });
return promiseifyTxn(txn); await promiseifyTxn(txn);
} }
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> { public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
let result; let result: IProblem | null = null;
const txn = this.db.transaction("session_problems", "readwrite"); const txn = this.db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems"); const objectStore = txn.objectStore("session_problems");
const index = objectStore.index("deviceKey"); const index = objectStore.index("deviceKey");
@ -604,8 +604,8 @@ export class Backend implements CryptoStore {
txn: IDBTransaction, txn: IDBTransaction,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void, func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void { ): void {
let session: InboundGroupSessionData | boolean = false; let session: InboundGroupSessionData | null | boolean = false;
let withheld: IWithheld | boolean = false; let withheld: IWithheld | null | boolean = false;
const objectStore = txn.objectStore("inbound_group_sessions"); const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]); const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() { getReq.onsuccess = function() {
@ -619,7 +619,7 @@ export class Backend implements CryptoStore {
func(session as InboundGroupSessionData, withheld as IWithheld); func(session as InboundGroupSessionData, withheld as IWithheld);
} }
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
}; };
@ -636,7 +636,7 @@ export class Backend implements CryptoStore {
func(session as InboundGroupSessionData, withheld as IWithheld); func(session as InboundGroupSessionData, withheld as IWithheld);
} }
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
}; };
} }
@ -654,14 +654,14 @@ export class Backend implements CryptoStore {
sessionData: cursor.value.session, sessionData: cursor.value.session,
}); });
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
cursor.continue(); cursor.continue();
} else { } else {
try { try {
func(null); func(null);
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
} }
}; };
@ -726,7 +726,7 @@ export class Backend implements CryptoStore {
try { try {
func(getReq.result || null); func(getReq.result || null);
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
}; };
} }
@ -754,7 +754,7 @@ export class Backend implements CryptoStore {
try { try {
func(rooms); func(rooms);
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, <Error>e);
} }
} }
}; };
@ -1050,7 +1050,7 @@ function abortWithException(txn: IDBTransaction, e: Error) {
} }
} }
function promiseifyTxn<T>(txn: IDBTransaction): Promise<T> { function promiseifyTxn<T>(txn: IDBTransaction): Promise<T | null> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
txn.oncomplete = () => { txn.oncomplete = () => {
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) { if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {

View File

@ -18,7 +18,7 @@ import { logger, PrefixedLogger } from '../../logger';
import { LocalStorageCryptoStore } from './localStorage-crypto-store'; import { LocalStorageCryptoStore } from './localStorage-crypto-store';
import { MemoryCryptoStore } from './memory-crypto-store'; import { MemoryCryptoStore } from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend'; import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
import { InvalidCryptoStoreError } from '../../errors'; import { InvalidCryptoStoreError, InvalidCryptoStoreState } from '../../errors';
import * as IndexedDBHelpers from "../../indexeddb-helpers"; import * as IndexedDBHelpers from "../../indexeddb-helpers";
import { import {
CryptoStore, CryptoStore,
@ -64,8 +64,8 @@ export class IndexedDBCryptoStore implements CryptoStore {
return IndexedDBHelpers.exists(indexedDB, dbName); return IndexedDBHelpers.exists(indexedDB, dbName);
} }
private backendPromise: Promise<CryptoStore> = null; private backendPromise?: Promise<CryptoStore>;
private backend: CryptoStore = null; private backend?: CryptoStore;
/** /**
* Create a new IndexedDBCryptoStore * Create a new IndexedDBCryptoStore
@ -141,7 +141,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
logger.warn("Crypto DB is too new for us to use!", e); logger.warn("Crypto DB is too new for us to use!", e);
// don't fall back to a different store: the user has crypto data // don't fall back to a different store: the user has crypto data
// in this db so we should use it or nothing at all. // in this db so we should use it or nothing at all.
throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW); throw new InvalidCryptoStoreError(InvalidCryptoStoreState.TooNew);
} }
logger.warn( logger.warn(
`unable to connect to indexeddb ${this.dbName}` + `unable to connect to indexeddb ${this.dbName}` +
@ -213,7 +213,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* same instance as passed in, or the existing one. * same instance as passed in, or the existing one.
*/ */
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> { public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
return this.backend.getOrAddOutgoingRoomKeyRequest(request); return this.backend!.getOrAddOutgoingRoomKeyRequest(request);
} }
/** /**
@ -227,7 +227,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* not found * not found
*/ */
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> { public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.getOutgoingRoomKeyRequest(requestBody); return this.backend!.getOutgoingRoomKeyRequest(requestBody);
} }
/** /**
@ -241,7 +241,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* requests in those states, an arbitrary one is chosen. * requests in those states, an arbitrary one is chosen.
*/ */
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> { public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.getOutgoingRoomKeyRequestByState(wantedStates); return this.backend!.getOutgoingRoomKeyRequestByState(wantedStates);
} }
/** /**
@ -252,7 +252,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @return {Promise<Array<*>>} Returns an array of requests in the given state * @return {Promise<Array<*>>} Returns an array of requests in the given state
*/ */
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> { public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return this.backend.getAllOutgoingRoomKeyRequestsByState(wantedState); return this.backend!.getAllOutgoingRoomKeyRequestsByState(wantedState);
} }
/** /**
@ -270,7 +270,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
deviceId: string, deviceId: string,
wantedStates: number[], wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> { ): Promise<OutgoingRoomKeyRequest[]> {
return this.backend.getOutgoingRoomKeyRequestsByTarget( return this.backend!.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates, userId, deviceId, wantedStates,
); );
} }
@ -292,7 +292,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
expectedState: number, expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>, updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> { ): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.updateOutgoingRoomKeyRequest( return this.backend!.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates, requestId, expectedState, updates,
); );
} }
@ -310,7 +310,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
requestId: string, requestId: string,
expectedState: number, expectedState: number,
): Promise<OutgoingRoomKeyRequest | null> { ): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.deleteOutgoingRoomKeyRequest(requestId, expectedState); return this.backend!.deleteOutgoingRoomKeyRequest(requestId, expectedState);
} }
// Olm Account // Olm Account
@ -323,7 +323,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {function(string)} func Called with the account pickle * @param {function(string)} func Called with the account pickle
*/ */
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void) { public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void) {
this.backend.getAccount(txn, func); this.backend!.getAccount(txn, func);
} }
/** /**
@ -334,7 +334,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {string} accountPickle The new account pickle to store. * @param {string} accountPickle The new account pickle to store.
*/ */
public storeAccount(txn: IDBTransaction, accountPickle: string): void { public storeAccount(txn: IDBTransaction, accountPickle: string): void {
this.backend.storeAccount(txn, accountPickle); this.backend!.storeAccount(txn, accountPickle);
} }
/** /**
@ -349,7 +349,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
txn: IDBTransaction, txn: IDBTransaction,
func: (keys: Record<string, ICrossSigningKey> | null) => void, func: (keys: Record<string, ICrossSigningKey> | null) => void,
): void { ): void {
this.backend.getCrossSigningKeys(txn, func); this.backend!.getCrossSigningKeys(txn, func);
} }
/** /**
@ -362,7 +362,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
func: (key: SecretStorePrivateKeys[K] | null) => void, func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K, type: K,
): void { ): void {
this.backend.getSecretStorePrivateKey(txn, func, type); this.backend!.getSecretStorePrivateKey(txn, func, type);
} }
/** /**
@ -372,7 +372,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {string} keys keys object as getCrossSigningKeys() * @param {string} keys keys object as getCrossSigningKeys()
*/ */
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void { public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
this.backend.storeCrossSigningKeys(txn, keys); this.backend!.storeCrossSigningKeys(txn, keys);
} }
/** /**
@ -387,7 +387,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
type: K, type: K,
key: SecretStorePrivateKeys[K], key: SecretStorePrivateKeys[K],
): void { ): void {
this.backend.storeSecretStorePrivateKey(txn, type, key); this.backend!.storeSecretStorePrivateKey(txn, type, key);
} }
// Olm sessions // Olm sessions
@ -398,7 +398,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {function(int)} func Called with the count of sessions * @param {function(int)} func Called with the count of sessions
*/ */
public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void { public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
this.backend.countEndToEndSessions(txn, func); this.backend!.countEndToEndSessions(txn, func);
} }
/** /**
@ -417,9 +417,9 @@ export class IndexedDBCryptoStore implements CryptoStore {
deviceKey: string, deviceKey: string,
sessionId: string, sessionId: string,
txn: IDBTransaction, txn: IDBTransaction,
func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void, func: (session: ISessionInfo | null) => void,
): void { ): void {
this.backend.getEndToEndSession(deviceKey, sessionId, txn, func); this.backend!.getEndToEndSession(deviceKey, sessionId, txn, func);
} }
/** /**
@ -438,7 +438,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
txn: IDBTransaction, txn: IDBTransaction,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void, func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void { ): void {
this.backend.getEndToEndSessions(deviceKey, txn, func); this.backend!.getEndToEndSessions(deviceKey, txn, func);
} }
/** /**
@ -448,8 +448,8 @@ export class IndexedDBCryptoStore implements CryptoStore {
* an object with, deviceKey, lastReceivedMessageTs, sessionId * an object with, deviceKey, lastReceivedMessageTs, sessionId
* and session keys. * and session keys.
*/ */
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void { public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
this.backend.getAllEndToEndSessions(txn, func); this.backend!.getAllEndToEndSessions(txn, func);
} }
/** /**
@ -465,19 +465,19 @@ export class IndexedDBCryptoStore implements CryptoStore {
sessionInfo: ISessionInfo, sessionInfo: ISessionInfo,
txn: IDBTransaction, txn: IDBTransaction,
): void { ): void {
this.backend.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn); this.backend!.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn);
} }
public storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> { public storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
return this.backend.storeEndToEndSessionProblem(deviceKey, type, fixed); return this.backend!.storeEndToEndSessionProblem(deviceKey, type, fixed);
} }
public getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> { public getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
return this.backend.getEndToEndSessionProblem(deviceKey, timestamp); return this.backend!.getEndToEndSessionProblem(deviceKey, timestamp);
} }
public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> { public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
return this.backend.filterOutNotifiedErrorDevices(devices); return this.backend!.filterOutNotifiedErrorDevices(devices);
} }
// Inbound group sessions // Inbound group sessions
@ -497,7 +497,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
txn: IDBTransaction, txn: IDBTransaction,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void, func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void { ): void {
this.backend.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func); this.backend!.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func);
} }
/** /**
@ -511,7 +511,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
txn: IDBTransaction, txn: IDBTransaction,
func: (session: ISession | null) => void, func: (session: ISession | null) => void,
): void { ): void {
this.backend.getAllEndToEndInboundGroupSessions(txn, func); this.backend!.getAllEndToEndInboundGroupSessions(txn, func);
} }
/** /**
@ -529,7 +529,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
sessionData: InboundGroupSessionData, sessionData: InboundGroupSessionData,
txn: IDBTransaction, txn: IDBTransaction,
): void { ): void {
this.backend.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); this.backend!.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
} }
/** /**
@ -547,7 +547,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
sessionData: InboundGroupSessionData, sessionData: InboundGroupSessionData,
txn: IDBTransaction, txn: IDBTransaction,
): void { ): void {
this.backend.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); this.backend!.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
} }
public storeEndToEndInboundGroupSessionWithheld( public storeEndToEndInboundGroupSessionWithheld(
@ -556,7 +556,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
sessionData: IWithheld, sessionData: IWithheld,
txn: IDBTransaction, txn: IDBTransaction,
): void { ): void {
this.backend.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn); this.backend!.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn);
} }
// End-to-end device tracking // End-to-end device tracking
@ -572,7 +572,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
*/ */
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void { public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
this.backend.storeEndToEndDeviceData(deviceData, txn); this.backend!.storeEndToEndDeviceData(deviceData, txn);
} }
/** /**
@ -583,7 +583,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* device data * device data
*/ */
public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void { public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
this.backend.getEndToEndDeviceData(txn, func); this.backend!.getEndToEndDeviceData(txn, func);
} }
// End to End Rooms // End to End Rooms
@ -595,7 +595,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
*/ */
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void { public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
this.backend.storeEndToEndRoom(roomId, roomInfo, txn); this.backend!.storeEndToEndRoom(roomId, roomInfo, txn);
} }
/** /**
@ -604,7 +604,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {function(Object)} func Function called with the end to end encrypted rooms * @param {function(Object)} func Function called with the end to end encrypted rooms
*/ */
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void { public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
this.backend.getEndToEndRooms(txn, func); this.backend!.getEndToEndRooms(txn, func);
} }
// session backups // session backups
@ -616,7 +616,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @returns {Promise} resolves to an array of inbound group sessions * @returns {Promise} resolves to an array of inbound group sessions
*/ */
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> { public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
return this.backend.getSessionsNeedingBackup(limit); return this.backend!.getSessionsNeedingBackup(limit);
} }
/** /**
@ -625,7 +625,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @returns {Promise} resolves to the number of sessions * @returns {Promise} resolves to the number of sessions
*/ */
public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> { public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
return this.backend.countSessionsNeedingBackup(txn); return this.backend!.countSessionsNeedingBackup(txn);
} }
/** /**
@ -635,7 +635,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @returns {Promise} resolves when the sessions are unmarked * @returns {Promise} resolves when the sessions are unmarked
*/ */
public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> { public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this.backend.unmarkSessionsNeedingBackup(sessions, txn); return this.backend!.unmarkSessionsNeedingBackup(sessions, txn);
} }
/** /**
@ -645,7 +645,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @returns {Promise} resolves when the sessions are marked * @returns {Promise} resolves when the sessions are marked
*/ */
public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> { public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this.backend.markSessionsNeedingBackup(sessions, txn); return this.backend!.markSessionsNeedingBackup(sessions, txn);
} }
/** /**
@ -661,7 +661,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
sessionId: string, sessionId: string,
txn?: IDBTransaction, txn?: IDBTransaction,
): void { ): void {
this.backend.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn); this.backend!.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn);
} }
/** /**
@ -674,7 +674,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
roomId: string, roomId: string,
txn?: IDBTransaction, txn?: IDBTransaction,
): Promise<[senderKey: string, sessionId: string][]> { ): Promise<[senderKey: string, sessionId: string][]> {
return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn); return this.backend!.getSharedHistoryInboundGroupSessions(roomId, txn);
} }
/** /**
@ -685,7 +685,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
parkedData: ParkedSharedHistory, parkedData: ParkedSharedHistory,
txn?: IDBTransaction, txn?: IDBTransaction,
): void { ): void {
this.backend.addParkedSharedHistory(roomId, parkedData, txn); this.backend!.addParkedSharedHistory(roomId, parkedData, txn);
} }
/** /**
@ -695,7 +695,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
roomId: string, roomId: string,
txn?: IDBTransaction, txn?: IDBTransaction,
): Promise<ParkedSharedHistory[]> { ): Promise<ParkedSharedHistory[]> {
return this.backend.takeParkedSharedHistory(roomId, txn); return this.backend!.takeParkedSharedHistory(roomId, txn);
} }
/** /**
@ -720,7 +720,12 @@ export class IndexedDBCryptoStore implements CryptoStore {
* reject with that exception. On synchronous backends, the * reject with that exception. On synchronous backends, the
* exception will propagate to the caller of the getFoo method. * exception will propagate to the caller of the getFoo method.
*/ */
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: IDBTransaction) => T, log?: PrefixedLogger): Promise<T> { public doTxn<T>(
return this.backend.doTxn(mode, stores, func, log); mode: Mode,
stores: Iterable<string>,
func: (txn: IDBTransaction) => T,
log?: PrefixedLogger,
): Promise<T> {
return this.backend!.doTxn<T>(mode, stores, func as (txn: unknown) => T, log);
} }
} }

View File

@ -69,7 +69,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
public static exists(store: Storage): boolean { public static exists(store: Storage): boolean {
const length = store.length; const length = store.length;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
if (store.key(i).startsWith(E2E_PREFIX)) { if (store.key(i)?.startsWith(E2E_PREFIX)) {
return true; return true;
} }
} }
@ -85,7 +85,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
public countEndToEndSessions(txn: unknown, func: (count: number) => void): void { public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
let count = 0; let count = 0;
for (let i = 0; i < this.store.length; ++i) { for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count; if (this.store.key(i)?.startsWith(keyEndToEndSessions(''))) ++count;
} }
func(count); func(count);
} }
@ -129,8 +129,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void { public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
for (let i = 0; i < this.store.length; ++i) { for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) { if (this.store.key(i)?.startsWith(keyEndToEndSessions(''))) {
const deviceKey = this.store.key(i).split('/')[1]; const deviceKey = this.store.key(i)!.split('/')[1];
for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) { for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) {
func(sess); func(sess);
} }
@ -220,7 +220,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void { public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
for (let i = 0; i < this.store.length; ++i) { for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i); const key = this.store.key(i);
if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) { if (key?.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
// we can't use split, as the components we are trying to split out // we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the // might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded // senderKey being a (32-byte) curve25519 key, base64-encoded
@ -229,7 +229,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func({ func({
senderKey: key.slice(KEY_INBOUND_SESSION_PREFIX.length, KEY_INBOUND_SESSION_PREFIX.length + 43), senderKey: key.slice(KEY_INBOUND_SESSION_PREFIX.length, KEY_INBOUND_SESSION_PREFIX.length + 43),
sessionId: key.slice(KEY_INBOUND_SESSION_PREFIX.length + 44), sessionId: key.slice(KEY_INBOUND_SESSION_PREFIX.length + 44),
sessionData: getJsonItem(this.store, key), sessionData: getJsonItem(this.store, key)!,
}); });
} }
} }
@ -297,9 +297,9 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
for (let i = 0; i < this.store.length; ++i) { for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i); const key = this.store.key(i);
if (key.startsWith(prefix)) { if (key?.startsWith(prefix)) {
const roomId = key.slice(prefix.length); const roomId = key.slice(prefix.length);
result[roomId] = getJsonItem(this.store, key); result[roomId] = getJsonItem(this.store, key)!;
} }
} }
func(result); func(result);
@ -320,7 +320,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
sessions.push({ sessions.push({
senderKey: senderKey, senderKey: senderKey,
sessionId: sessionId, sessionId: sessionId,
sessionData: sessionData, sessionData: sessionData!,
}); });
}, },
); );
@ -417,10 +417,10 @@ function getJsonItem<T>(store: Storage, key: string): T | null {
try { try {
// if the key is absent, store.getItem() returns null, and // if the key is absent, store.getItem() returns null, and
// JSON.parse(null) === null, so this returns null. // JSON.parse(null) === null, so this returns null.
return JSON.parse(store.getItem(key)); return JSON.parse(store.getItem(key)!);
} catch (e) { } catch (e) {
logger.log("Error: Failed to get key %s: %s", key, e.stack || e); logger.log("Error: Failed to get key %s: %s", key, (<Error>e).message);
logger.log(e.stack); logger.log((<Error>e).stack);
} }
return null; return null;
} }

View File

@ -54,7 +54,7 @@ export class MemoryCryptoStore implements CryptoStore {
private inboundGroupSessions: { [sessionKey: string]: InboundGroupSessionData } = {}; private inboundGroupSessions: { [sessionKey: string]: InboundGroupSessionData } = {};
private inboundGroupSessionsWithheld: Record<string, IWithheld> = {}; private inboundGroupSessionsWithheld: Record<string, IWithheld> = {};
// Opaque device data object // Opaque device data object
private deviceData: IDeviceData = null; private deviceData: IDeviceData | null = null;
private rooms: { [roomId: string]: IRoomEncryption } = {}; private rooms: { [roomId: string]: IRoomEncryption } = {};
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {}; private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {}; private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};

View File

@ -55,14 +55,14 @@ export class VerificationBase<
> extends TypedEventEmitter<Events | VerificationEvent, Arguments, VerificationEventHandlerMap> { > extends TypedEventEmitter<Events | VerificationEvent, Arguments, VerificationEventHandlerMap> {
private cancelled = false; private cancelled = false;
private _done = false; private _done = false;
private promise: Promise<void> = null; private promise: Promise<void> | null = null;
private transactionTimeoutTimer: ReturnType<typeof setTimeout> = null; private transactionTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
protected expectedEvent: string; protected expectedEvent?: string;
private resolve: () => void; private resolve?: () => void;
private reject: (e: Error | MatrixEvent) => void; private reject?: (e: Error | MatrixEvent) => void;
private resolveEvent: (e: MatrixEvent) => void; private resolveEvent?: (e: MatrixEvent) => void;
private rejectEvent: (e: Error) => void; private rejectEvent?: (e: Error) => void;
private started: boolean; private started?: boolean;
/** /**
* Base class for verification methods. * Base class for verification methods.
@ -187,7 +187,7 @@ export class VerificationBase<
this.expectedEvent = undefined; this.expectedEvent = undefined;
this.rejectEvent = undefined; this.rejectEvent = undefined;
this.resetTimer(); this.resetTimer();
this.resolveEvent(e); this.resolveEvent?.(e);
} }
} else if (e.getType() === EventType.KeyVerificationCancel) { } else if (e.getType() === EventType.KeyVerificationCancel) {
const reject = this.reject; const reject = this.reject;
@ -218,11 +218,11 @@ export class VerificationBase<
} }
} }
public done(): Promise<KeysDuringVerification | void> { public async done(): Promise<KeysDuringVerification | void> {
this.endTimer(); // always kill the activity timer this.endTimer(); // always kill the activity timer
if (!this._done) { if (!this._done) {
this.request.onVerifierFinished(); this.request.onVerifierFinished();
this.resolve(); this.resolve?.();
return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId); return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId);
} }
} }
@ -291,7 +291,7 @@ export class VerificationBase<
this.endTimer(); this.endTimer();
resolve(...args); resolve(...args);
}; };
this.reject = (e: Error) => { this.reject = (e: Error | MatrixEvent) => {
this._done = true; this._done = true;
this.endTimer(); this.endTimer();
reject(e); reject(e);
@ -301,12 +301,12 @@ export class VerificationBase<
this.started = true; this.started = true;
this.resetTimer(); // restart the timeout this.resetTimer(); // restart the timeout
new Promise<void>((resolve, reject) => { new Promise<void>((resolve, reject) => {
const crossSignId = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(this.userId)?.getId(); const crossSignId = this.baseApis.crypto!.deviceList.getStoredCrossSigningForUser(this.userId)?.getId();
if (crossSignId === this.deviceId) { if (crossSignId === this.deviceId) {
reject(new Error("Device ID is the same as the cross-signing ID")); reject(new Error("Device ID is the same as the cross-signing ID"));
} }
resolve(); resolve();
}).then(() => this.doVerification()).then(this.done.bind(this), this.cancel.bind(this)); }).then(() => this.doVerification!()).then(this.done.bind(this), this.cancel.bind(this));
} }
return this.promise; return this.promise;
} }
@ -326,7 +326,7 @@ export class VerificationBase<
verifier(keyId, device, keyInfo); verifier(keyId, device, keyInfo);
verifiedDevices.push([deviceId, keyId, device.keys[keyId]]); verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
} else { } else {
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId); const crossSigningInfo = this.baseApis.crypto!.deviceList.getStoredCrossSigningForUser(userId);
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
verifier(keyId, DeviceInfo.fromStorage({ verifier(keyId, DeviceInfo.fromStorage({
keys: { keys: {
@ -356,7 +356,7 @@ export class VerificationBase<
// to upload each signature in a separate API call which is silly because the // to upload each signature in a separate API call which is silly because the
// API supports as many signatures as you like. // API supports as many signatures as you like.
for (const [deviceId, keyId, key] of verifiedDevices) { for (const [deviceId, keyId, key] of verifiedDevices) {
await this.baseApis.crypto.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key }); await this.baseApis.crypto!.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key });
} }
// if one of the user's own devices is being marked as verified / unverified, // if one of the user's own devices is being marked as verified / unverified,

View File

@ -49,7 +49,7 @@ type EventHandlerMap = {
* @extends {module:crypto/verification/Base} * @extends {module:crypto/verification/Base}
*/ */
export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> { export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
public reciprocateQREvent: IReciprocateQr; public reciprocateQREvent?: IReciprocateQr;
public static factory( public static factory(
channel: IVerificationChannel, channel: IVerificationChannel,
@ -76,7 +76,7 @@ export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
const { qrCodeData } = this.request; const { qrCodeData } = this.request;
// 1. check the secret // 1. check the secret
if (this.startEvent.getContent()['secret'] !== qrCodeData.encodedSharedSecret) { if (this.startEvent.getContent()['secret'] !== qrCodeData?.encodedSharedSecret) {
throw newKeyMismatchError(); throw newKeyMismatchError();
} }
@ -92,21 +92,21 @@ export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
// 3. determine key to sign / mark as trusted // 3. determine key to sign / mark as trusted
const keys: Record<string, string> = {}; const keys: Record<string, string> = {};
switch (qrCodeData.mode) { switch (qrCodeData?.mode) {
case Mode.VerifyOtherUser: { case Mode.VerifyOtherUser: {
// add master key to keys to be signed, only if we're not doing self-verification // add master key to keys to be signed, only if we're not doing self-verification
const masterKey = qrCodeData.otherUserMasterKey; const masterKey = qrCodeData.otherUserMasterKey;
keys[`ed25519:${masterKey}`] = masterKey; keys[`ed25519:${masterKey}`] = masterKey!;
break; break;
} }
case Mode.VerifySelfTrusted: { case Mode.VerifySelfTrusted: {
const deviceId = this.request.targetDevice.deviceId; const deviceId = this.request.targetDevice.deviceId;
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey; keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey!;
break; break;
} }
case Mode.VerifySelfUntrusted: { case Mode.VerifySelfUntrusted: {
const masterKey = qrCodeData.myMasterKey; const masterKey = qrCodeData.myMasterKey;
keys[`ed25519:${masterKey}`] = masterKey; keys[`ed25519:${masterKey}`] = masterKey!;
break; break;
} }
} }
@ -158,41 +158,41 @@ export class QRCodeData {
public readonly mode: Mode, public readonly mode: Mode,
private readonly sharedSecret: string, private readonly sharedSecret: string,
// only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code // only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
public readonly otherUserMasterKey: string | undefined, public readonly otherUserMasterKey: string | null,
// only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code // only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code
public readonly otherDeviceKey: string | undefined, public readonly otherDeviceKey: string | null,
// only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code // only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code
public readonly myMasterKey: string | undefined, public readonly myMasterKey: string | null,
private readonly buffer: Buffer, private readonly buffer: Buffer,
) {} ) {}
public static async create(request: VerificationRequest, client: MatrixClient): Promise<QRCodeData> { public static async create(request: VerificationRequest, client: MatrixClient): Promise<QRCodeData> {
const sharedSecret = QRCodeData.generateSharedSecret(); const sharedSecret = QRCodeData.generateSharedSecret();
const mode = QRCodeData.determineMode(request, client); const mode = QRCodeData.determineMode(request, client);
let otherUserMasterKey = null; let otherUserMasterKey: string | null = null;
let otherDeviceKey = null; let otherDeviceKey: string | null = null;
let myMasterKey = null; let myMasterKey: string | null = null;
if (mode === Mode.VerifyOtherUser) { if (mode === Mode.VerifyOtherUser) {
const otherUserCrossSigningInfo = const otherUserCrossSigningInfo = client.getStoredCrossSigningForUser(request.otherUserId);
client.getStoredCrossSigningForUser(request.otherUserId); otherUserMasterKey = otherUserCrossSigningInfo!.getId("master");
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
} else if (mode === Mode.VerifySelfTrusted) { } else if (mode === Mode.VerifySelfTrusted) {
otherDeviceKey = await QRCodeData.getOtherDeviceKey(request, client); otherDeviceKey = await QRCodeData.getOtherDeviceKey(request, client);
} else if (mode === Mode.VerifySelfUntrusted) { } else if (mode === Mode.VerifySelfUntrusted) {
const myUserId = client.getUserId(); const myUserId = client.getUserId()!;
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId); const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
myMasterKey = myCrossSigningInfo.getId("master"); myMasterKey = myCrossSigningInfo!.getId("master");
} }
const qrData = QRCodeData.generateQrData( const qrData = QRCodeData.generateQrData(
request, client, mode, request,
client,
mode,
sharedSecret, sharedSecret,
otherUserMasterKey, otherUserMasterKey!,
otherDeviceKey, otherDeviceKey!,
myMasterKey, myMasterKey!,
); );
const buffer = QRCodeData.generateBuffer(qrData); const buffer = QRCodeData.generateBuffer(qrData);
return new QRCodeData(mode, sharedSecret, return new QRCodeData(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
} }
/** /**
@ -213,12 +213,11 @@ export class QRCodeData {
} }
private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise<string> { private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise<string> {
const myUserId = client.getUserId(); const myUserId = client.getUserId()!;
const otherDevice = request.targetDevice; const otherDevice = request.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null; const device = otherDevice.deviceId ? client.getStoredDevice(myUserId, otherDevice.deviceId) : undefined;
const device = client.getStoredDevice(myUserId, otherDeviceId);
if (!device) { if (!device) {
throw new Error("could not find device " + otherDeviceId); throw new Error("could not find device " + otherDevice?.deviceId);
} }
return device.getFingerprint(); return device.getFingerprint();
} }
@ -245,11 +244,11 @@ export class QRCodeData {
client: MatrixClient, client: MatrixClient,
mode: Mode, mode: Mode,
encodedSharedSecret: string, encodedSharedSecret: string,
otherUserMasterKey: string, otherUserMasterKey?: string,
otherDeviceKey: string, otherDeviceKey?: string,
myMasterKey: string, myMasterKey?: string,
): IQrData { ): IQrData {
const myUserId = client.getUserId(); const myUserId = client.getUserId()!;
const transactionId = request.channel.transactionId; const transactionId = request.channel.transactionId;
const qrData = { const qrData = {
prefix: BINARY_PREFIX, prefix: BINARY_PREFIX,
@ -265,18 +264,18 @@ export class QRCodeData {
if (mode === Mode.VerifyOtherUser) { if (mode === Mode.VerifyOtherUser) {
// First key is our master cross signing key // First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master"); qrData.firstKeyB64 = myCrossSigningInfo!.getId("master")!;
// Second key is the other user's master cross signing key // Second key is the other user's master cross signing key
qrData.secondKeyB64 = otherUserMasterKey; qrData.secondKeyB64 = otherUserMasterKey!;
} else if (mode === Mode.VerifySelfTrusted) { } else if (mode === Mode.VerifySelfTrusted) {
// First key is our master cross signing key // First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master"); qrData.firstKeyB64 = myCrossSigningInfo!.getId("master")!;
qrData.secondKeyB64 = otherDeviceKey; qrData.secondKeyB64 = otherDeviceKey!;
} else if (mode === Mode.VerifySelfUntrusted) { } else if (mode === Mode.VerifySelfUntrusted) {
// First key is our device's key // First key is our device's key
qrData.firstKeyB64 = client.getDeviceEd25519Key(); qrData.firstKeyB64 = client.getDeviceEd25519Key()!;
// Second key is what we think our master cross signing key is // Second key is what we think our master cross signing key is
qrData.secondKeyB64 = myMasterKey; qrData.secondKeyB64 = myMasterKey!;
} }
return qrData; return qrData;
} }

View File

@ -96,25 +96,25 @@ export class VerificationRequest<
private eventsByUs = new Map<string, MatrixEvent>(); private eventsByUs = new Map<string, MatrixEvent>();
private eventsByThem = new Map<string, MatrixEvent>(); private eventsByThem = new Map<string, MatrixEvent>();
private _observeOnly = false; private _observeOnly = false;
private timeoutTimer: ReturnType<typeof setTimeout> = null; private timeoutTimer: ReturnType<typeof setTimeout> | null = null;
private _accepting = false; private _accepting = false;
private _declining = false; private _declining = false;
private verifierHasFinished = false; private verifierHasFinished = false;
private _cancelled = false; private _cancelled = false;
private _chosenMethod: VerificationMethod = null; private _chosenMethod: VerificationMethod | null = null;
// we keep a copy of the QR Code data (including other user master key) around // we keep a copy of the QR Code data (including other user master key) around
// for QR reciprocate verification, to protect against // for QR reciprocate verification, to protect against
// cross-signing identity reset between the .ready and .start event // cross-signing identity reset between the .ready and .start event
// and signing the wrong key after .start // and signing the wrong key after .start
private _qrCodeData: QRCodeData = null; private _qrCodeData: QRCodeData | null = null;
// The timestamp when we received the request event from the other side // The timestamp when we received the request event from the other side
private requestReceivedAt: number = null; private requestReceivedAt: number | null = null;
private commonMethods: VerificationMethod[] = []; private commonMethods: VerificationMethod[] = [];
private _phase: Phase; private _phase: Phase;
public _cancellingUserId: string; // Used in tests only public _cancellingUserId: string; // Used in tests only
private _verifier: VerificationBase<any, any>; private _verifier?: VerificationBase<any, any>;
constructor( constructor(
public readonly channel: C, public readonly channel: C,
@ -204,7 +204,7 @@ export class VerificationRequest<
} }
/** the method picked in the .start event */ /** the method picked in the .start event */
public get chosenMethod(): VerificationMethod { public get chosenMethod(): VerificationMethod | null {
return this._chosenMethod; return this._chosenMethod;
} }
@ -236,7 +236,7 @@ export class VerificationRequest<
* The key verification request event. * The key verification request event.
* @returns {MatrixEvent} The request event, or falsey if not found. * @returns {MatrixEvent} The request event, or falsey if not found.
*/ */
public get requestEvent(): MatrixEvent { public get requestEvent(): MatrixEvent | undefined {
return this.getEventByEither(REQUEST_TYPE); return this.getEventByEither(REQUEST_TYPE);
} }
@ -246,7 +246,7 @@ export class VerificationRequest<
} }
/** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */ /** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
public get verifier(): VerificationBase<any, any> { public get verifier(): VerificationBase<any, any> | undefined {
return this._verifier; return this._verifier;
} }
@ -270,7 +270,7 @@ export class VerificationRequest<
} }
/** Only set after a .ready if the other party can scan a QR code */ /** Only set after a .ready if the other party can scan a QR code */
public get qrCodeData(): QRCodeData { public get qrCodeData(): QRCodeData | null {
return this._qrCodeData; return this._qrCodeData;
} }
@ -340,7 +340,7 @@ export class VerificationRequest<
/** The id of the user that initiated the request */ /** The id of the user that initiated the request */
public get requestingUserId(): string { public get requestingUserId(): string {
if (this.initiatedByMe) { if (this.initiatedByMe) {
return this.client.getUserId(); return this.client.getUserId()!;
} else { } else {
return this.otherUserId; return this.otherUserId;
} }
@ -351,7 +351,7 @@ export class VerificationRequest<
if (this.initiatedByMe) { if (this.initiatedByMe) {
return this.otherUserId; return this.otherUserId;
} else { } else {
return this.client.getUserId(); return this.client.getUserId()!;
} }
} }
@ -368,7 +368,7 @@ export class VerificationRequest<
* The id of the user that cancelled the request, * The id of the user that cancelled the request,
* only defined when phase is PHASE_CANCELLED * only defined when phase is PHASE_CANCELLED
*/ */
public get cancellingUserId(): string { public get cancellingUserId(): string | undefined {
const myCancel = this.eventsByUs.get(CANCEL_TYPE); const myCancel = this.eventsByUs.get(CANCEL_TYPE);
const theirCancel = this.eventsByThem.get(CANCEL_TYPE); const theirCancel = this.eventsByThem.get(CANCEL_TYPE);
@ -422,7 +422,7 @@ export class VerificationRequest<
*/ */
public beginKeyVerification( public beginKeyVerification(
method: VerificationMethod, method: VerificationMethod,
targetDevice: ITargetDevice = null, targetDevice: ITargetDevice | null = null,
): VerificationBase<any, any> { ): VerificationBase<any, any> {
// need to allow also when unsent in case of to_device // need to allow also when unsent in case of to_device
if (!this.observeOnly && !this._verifier) { if (!this.observeOnly && !this._verifier) {
@ -443,7 +443,7 @@ export class VerificationRequest<
this._chosenMethod = method; this._chosenMethod = method;
} }
} }
return this._verifier; return this._verifier!;
} }
/** /**
@ -470,7 +470,7 @@ export class VerificationRequest<
if (this._verifier) { if (this._verifier) {
return this._verifier.cancel(errorFactory(code, reason)()); return this._verifier.cancel(errorFactory(code, reason)());
} else { } else {
this._cancellingUserId = this.client.getUserId(); this._cancellingUserId = this.client.getUserId()!;
await this.channel.send(CANCEL_TYPE, { code, reason }); await this.channel.send(CANCEL_TYPE, { code, reason });
} }
} }
@ -525,11 +525,11 @@ export class VerificationRequest<
} }
} }
private getEventByEither(type: string): MatrixEvent { private getEventByEither(type: string): MatrixEvent | undefined {
return this.eventsByThem.get(type) || this.eventsByUs.get(type); return this.eventsByThem.get(type) || this.eventsByUs.get(type);
} }
private getEventBy(type: string, byThem = false): MatrixEvent { private getEventBy(type: string, byThem = false): MatrixEvent | undefined {
if (byThem) { if (byThem) {
return this.eventsByThem.get(type); return this.eventsByThem.get(type);
} else { } else {
@ -548,20 +548,18 @@ export class VerificationRequest<
transitions.push({ phase: PHASE_REQUESTED, event: requestEvent }); transitions.push({ phase: PHASE_REQUESTED, event: requestEvent });
} }
const readyEvent = const readyEvent = requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem);
requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem);
if (readyEvent && phase() === PHASE_REQUESTED) { if (readyEvent && phase() === PHASE_REQUESTED) {
transitions.push({ phase: PHASE_READY, event: readyEvent }); transitions.push({ phase: PHASE_READY, event: readyEvent });
} }
let startEvent; let startEvent: MatrixEvent | undefined;
if (readyEvent || !requestEvent) { if (readyEvent || !requestEvent) {
const theirStartEvent = this.eventsByThem.get(START_TYPE); const theirStartEvent = this.eventsByThem.get(START_TYPE);
const ourStartEvent = this.eventsByUs.get(START_TYPE); const ourStartEvent = this.eventsByUs.get(START_TYPE);
// any party can send .start after a .ready or unsent // any party can send .start after a .ready or unsent
if (theirStartEvent && ourStartEvent) { if (theirStartEvent && ourStartEvent) {
startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ? startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ? theirStartEvent : ourStartEvent;
theirStartEvent : ourStartEvent;
} else { } else {
startEvent = theirStartEvent ? theirStartEvent : ourStartEvent; startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
} }
@ -569,7 +567,9 @@ export class VerificationRequest<
startEvent = this.getEventBy(START_TYPE, !hasRequestByThem); startEvent = this.getEventBy(START_TYPE, !hasRequestByThem);
} }
if (startEvent) { if (startEvent) {
const fromRequestPhase = phase() === PHASE_REQUESTED && requestEvent.getSender() !== startEvent.getSender(); const fromRequestPhase = (
phase() === PHASE_REQUESTED && requestEvent?.getSender() !== startEvent.getSender()
);
const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE); const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE);
if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) { if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
transitions.push({ phase: PHASE_STARTED, event: startEvent }); transitions.push({ phase: PHASE_STARTED, event: startEvent });
@ -651,7 +651,7 @@ export class VerificationRequest<
if (newEvent.getType() !== START_TYPE) { if (newEvent.getType() !== START_TYPE) {
return false; return false;
} }
const oldEvent = this._verifier.startEvent; const oldEvent = this._verifier!.startEvent;
let oldRaceIdentifier; let oldRaceIdentifier;
if (this.isSelfVerification) { if (this.isSelfVerification) {
@ -890,9 +890,9 @@ export class VerificationRequest<
private createVerifier( private createVerifier(
method: VerificationMethod, method: VerificationMethod,
startEvent: MatrixEvent = null, startEvent: MatrixEvent | null = null,
targetDevice: ITargetDevice = null, targetDevice: ITargetDevice | null = null,
): VerificationBase<any, any> { ): VerificationBase<any, any> | undefined {
if (!targetDevice) { if (!targetDevice) {
targetDevice = this.targetDevice; targetDevice = this.targetDevice;
} }
@ -941,7 +941,7 @@ export class VerificationRequest<
} }
} }
public getEventFromOtherParty(type: string): MatrixEvent { public getEventFromOtherParty(type: string): MatrixEvent | undefined {
return this.eventsByThem.get(type); return this.eventsByThem.get(type);
} }
} }

View File

@ -1,52 +0,0 @@
// can't just do InvalidStoreError extends Error
// because of http://babeljs.io/docs/usage/caveats/#classes
export function InvalidStoreError(reason, value) {
const message = `Store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
const instance = Reflect.construct(Error, [message]);
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
instance.reason = reason;
instance.value = value;
return instance;
}
InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING";
InvalidStoreError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Reflect.setPrototypeOf(InvalidStoreError, Error);
export function InvalidCryptoStoreError(reason) {
const message = `Crypto store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
const instance = Reflect.construct(Error, [message]);
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
instance.reason = reason;
instance.name = 'InvalidCryptoStoreError';
return instance;
}
InvalidCryptoStoreError.TOO_NEW = "TOO_NEW";
InvalidCryptoStoreError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Reflect.setPrototypeOf(InvalidCryptoStoreError, Error);
export class KeySignatureUploadError extends Error {
constructor(message, value) {
super(message);
this.value = value;
}
}

51
src/errors.ts Normal file
View File

@ -0,0 +1,51 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum InvalidStoreState {
ToggledLazyLoading,
}
export class InvalidStoreError extends Error {
public static TOGGLED_LAZY_LOADING = InvalidStoreState.ToggledLazyLoading;
public constructor(public readonly reason: InvalidStoreState, public readonly value: any) {
const message = `Store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
super(message);
this.name = "InvalidStoreError";
}
}
export enum InvalidCryptoStoreState {
TooNew = "TOO_NEW",
}
export class InvalidCryptoStoreError extends Error {
public static TOO_NEW = InvalidCryptoStoreState.TooNew;
public constructor(public readonly reason: InvalidCryptoStoreState) {
const message = `Crypto store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
super(message);
this.name = 'InvalidCryptoStoreError';
}
}
export class KeySignatureUploadError extends Error {
public constructor(message: string, public readonly value: any) {
super(message);
}
}

View File

@ -36,11 +36,11 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
const room = client.getRoom(plainOldJsObject.room_id); const room = client.getRoom(plainOldJsObject.room_id);
let event: MatrixEvent; let event: MatrixEvent | undefined;
// If the event is already known to the room, let's re-use the model rather than duplicating. // If the event is already known to the room, let's re-use the model rather than duplicating.
// We avoid doing this to state events as they may be forward or backwards looking which tweaks behaviour. // We avoid doing this to state events as they may be forward or backwards looking which tweaks behaviour.
if (room && plainOldJsObject.state_key === undefined) { if (room && plainOldJsObject.state_key === undefined) {
event = room.findEventById(plainOldJsObject.event_id); event = room.findEventById(plainOldJsObject.event_id!);
} }
if (!event || event.status) { if (!event || event.status) {

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import { IUsageLimit } from "../@types/partials"; import { IUsageLimit } from "../@types/partials";
import { MatrixEvent } from "../models/event";
interface IErrorJson extends Partial<IUsageLimit> { interface IErrorJson extends Partial<IUsageLimit> {
[key: string]: any; // extensible [key: string]: any; // extensible
@ -50,7 +51,12 @@ export class MatrixError extends HTTPError {
public readonly errcode?: string; public readonly errcode?: string;
public readonly data: IErrorJson; public readonly data: IErrorJson;
constructor(errorJson: IErrorJson = {}, public readonly httpStatus?: number, public url?: string) { constructor(
errorJson: IErrorJson = {},
public readonly httpStatus?: number,
public url?: string,
public event?: MatrixEvent,
) {
let message = errorJson.error || "Unknown message"; let message = errorJson.error || "Unknown message";
if (httpStatus) { if (httpStatus) {
message = `[${httpStatus}] ${message}`; message = `[${httpStatus}] ${message}`;

View File

@ -73,7 +73,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
public idServerRequest<T extends {}>( public idServerRequest<T extends {}>(
method: Method, method: Method,
path: string, path: string,
params: Record<string, string | string[]>, params: Record<string, string | string[]> | undefined,
prefix: string, prefix: string,
accessToken?: string, accessToken?: string,
): Promise<ResponseType<T, O>> { ): Promise<ResponseType<T, O>> {
@ -96,7 +96,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
headers: {}, headers: {},
}; };
if (accessToken) { if (accessToken) {
opts.headers.Authorization = `Bearer ${accessToken}`; opts.headers!.Authorization = `Bearer ${accessToken}`;
} }
return this.requestOtherUrl(method, fullUri, body, opts); return this.requestOtherUrl(method, fullUri, body, opts);
@ -286,10 +286,10 @@ export class FetchHttpApi<O extends IHttpOpts> {
credentials: "omit", // we send credentials via headers credentials: "omit", // we send credentials via headers
}); });
} catch (e) { } catch (e) {
if (e.name === "AbortError") { if ((<Error>e).name === "AbortError") {
throw e; throw e;
} }
throw new ConnectionError("fetch failed", e); throw new ConnectionError("fetch failed", <Error>e);
} finally { } finally {
cleanup(); cleanup();
} }

View File

@ -72,11 +72,11 @@ export function anySignal(signals: AbortSignal[]): {
* @returns {Error} * @returns {Error}
*/ */
export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error { export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error {
let contentType: ParsedMediaType; let contentType: ParsedMediaType | null;
try { try {
contentType = getResponseContentType(response); contentType = getResponseContentType(response);
} catch (e) { } catch (e) {
return e; return <Error>e;
} }
if (contentType?.type === "application/json" && body) { if (contentType?.type === "application/json" && body) {

View File

@ -59,16 +59,16 @@ log.methodFactory = function(methodName, logLevel, loggerName) {
* Drop-in replacement for <code>console</code> using {@link https://www.npmjs.com/package/loglevel|loglevel}. * Drop-in replacement for <code>console</code> using {@link https://www.npmjs.com/package/loglevel|loglevel}.
* Can be tailored down to specific use cases if needed. * Can be tailored down to specific use cases if needed.
*/ */
export const logger: PrefixedLogger = log.getLogger(DEFAULT_NAMESPACE); export const logger = log.getLogger(DEFAULT_NAMESPACE) as PrefixedLogger;
logger.setLevel(log.levels.DEBUG, false); logger.setLevel(log.levels.DEBUG, false);
export interface PrefixedLogger extends Logger { export interface PrefixedLogger extends Logger {
withPrefix?: (prefix: string) => PrefixedLogger; withPrefix: (prefix: string) => PrefixedLogger;
prefix?: string; prefix: string;
} }
function extendLogger(logger: PrefixedLogger) { function extendLogger(logger: Logger) {
logger.withPrefix = function(prefix: string): PrefixedLogger { (<PrefixedLogger>logger).withPrefix = function(prefix: string): PrefixedLogger {
const existingPrefix = this.prefix || ""; const existingPrefix = this.prefix || "";
return getPrefixedLogger(existingPrefix + prefix); return getPrefixedLogger(existingPrefix + prefix);
}; };
@ -77,7 +77,7 @@ function extendLogger(logger: PrefixedLogger) {
extendLogger(logger); extendLogger(logger);
function getPrefixedLogger(prefix: string): PrefixedLogger { function getPrefixedLogger(prefix: string): PrefixedLogger {
const prefixLogger: PrefixedLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`); const prefixLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`) as PrefixedLogger;
if (prefixLogger.prefix !== prefix) { if (prefixLogger.prefix !== prefix) {
// Only do this setup work the first time through, as loggers are saved by name. // Only do this setup work the first time through, as loggers are saved by name.
extendLogger(prefixLogger); extendLogger(prefixLogger);

View File

@ -68,7 +68,7 @@ export function setCryptoStoreFactory(fac) {
} }
export interface ICryptoCallbacks { export interface ICryptoCallbacks {
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array>; getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void; saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
shouldUpgradeDeviceVerifications?: ( shouldUpgradeDeviceVerifications?: (
users: Record<string, any> users: Record<string, any>

View File

@ -86,7 +86,7 @@ export class MSC3089TreeSpace {
public readonly room: Room; public readonly room: Room;
public constructor(private client: MatrixClient, public readonly roomId: string) { public constructor(private client: MatrixClient, public readonly roomId: string) {
this.room = this.client.getRoom(this.roomId); this.room = this.client.getRoom(this.roomId)!;
if (!this.room) throw new Error("Unknown room"); if (!this.room) throw new Error("Unknown room");
} }
@ -282,7 +282,7 @@ export class MSC3089TreeSpace {
const members = this.room.currentState.getStateEvents(EventType.RoomMember); const members = this.room.currentState.getStateEvents(EventType.RoomMember);
for (const member of members) { for (const member of members) {
const isNotUs = member.getStateKey() !== this.client.getUserId(); const isNotUs = member.getStateKey() !== this.client.getUserId();
if (isNotUs && kickMemberships.includes(member.getContent().membership)) { if (isNotUs && kickMemberships.includes(member.getContent().membership!)) {
const stateKey = member.getStateKey(); const stateKey = member.getStateKey();
if (!stateKey) { if (!stateKey) {
throw new Error("State key not found for branch"); throw new Error("State key not found for branch");

View File

@ -51,21 +51,21 @@ export const getBeaconInfoIdentifier = (event: MatrixEvent): BeaconIdentifier =>
// https://github.com/matrix-org/matrix-spec-proposals/pull/3672 // https://github.com/matrix-org/matrix-spec-proposals/pull/3672
export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> { export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> {
public readonly roomId: string; public readonly roomId: string;
private _beaconInfo: BeaconInfoState; private _beaconInfo?: BeaconInfoState;
private _isLive: boolean; private _isLive?: boolean;
private livenessWatchTimeout: ReturnType<typeof setTimeout>; private livenessWatchTimeout?: ReturnType<typeof setTimeout>;
private _latestLocationEvent: MatrixEvent | undefined; private _latestLocationEvent?: MatrixEvent;
constructor( constructor(
private rootEvent: MatrixEvent, private rootEvent: MatrixEvent,
) { ) {
super(); super();
this.setBeaconInfo(this.rootEvent); this.setBeaconInfo(this.rootEvent);
this.roomId = this.rootEvent.getRoomId(); this.roomId = this.rootEvent.getRoomId()!;
} }
public get isLive(): boolean { public get isLive(): boolean {
return this._isLive; return !!this._isLive;
} }
public get identifier(): BeaconIdentifier { public get identifier(): BeaconIdentifier {
@ -77,14 +77,14 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
} }
public get beaconInfoOwner(): string { public get beaconInfoOwner(): string {
return this.rootEvent.getStateKey(); return this.rootEvent.getStateKey()!;
} }
public get beaconInfoEventType(): string { public get beaconInfoEventType(): string {
return this.rootEvent.getType(); return this.rootEvent.getType();
} }
public get beaconInfo(): BeaconInfoState { public get beaconInfo(): BeaconInfoState | undefined {
return this._beaconInfo; return this._beaconInfo;
} }
@ -101,7 +101,7 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
throw new Error('Invalid updating event'); throw new Error('Invalid updating event');
} }
// don't update beacon with an older event // don't update beacon with an older event
if (beaconInfoEvent.event.origin_server_ts < this.rootEvent.event.origin_server_ts) { if (beaconInfoEvent.getTs() < this.rootEvent.getTs()) {
return; return;
} }
this.rootEvent = beaconInfoEvent; this.rootEvent = beaconInfoEvent;
@ -130,20 +130,21 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
} }
this.checkLiveness(); this.checkLiveness();
if (!this.beaconInfo) return;
if (this.isLive) { if (this.isLive) {
const expiryInMs = (this._beaconInfo?.timestamp + this._beaconInfo?.timeout) - Date.now(); const expiryInMs = (this.beaconInfo.timestamp + this.beaconInfo.timeout) - Date.now();
if (expiryInMs > 1) { if (expiryInMs > 1) {
this.livenessWatchTimeout = setTimeout( this.livenessWatchTimeout = setTimeout(
() => { this.monitorLiveness(); }, () => { this.monitorLiveness(); },
expiryInMs, expiryInMs,
); );
} }
} else if (this._beaconInfo?.timestamp > Date.now()) { } else if (this.beaconInfo.timestamp > Date.now()) {
// beacon start timestamp is in the future // beacon start timestamp is in the future
// check liveness again then // check liveness again then
this.livenessWatchTimeout = setTimeout( this.livenessWatchTimeout = setTimeout(
() => { this.monitorLiveness(); }, () => { this.monitorLiveness(); },
this.beaconInfo?.timestamp - Date.now(), this.beaconInfo.timestamp - Date.now(),
); );
} }
} }
@ -165,22 +166,22 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
const { timestamp } = parsed; const { timestamp } = parsed;
return ( return (
// only include positions that were taken inside the beacon's live period // only include positions that were taken inside the beacon's live period
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) && isTimestampInDuration(this._beaconInfo!.timestamp, this._beaconInfo!.timeout, timestamp) &&
// ignore positions older than our current latest location // ignore positions older than our current latest location
(!this.latestLocationState || timestamp > this.latestLocationState.timestamp) (!this.latestLocationState || timestamp > this.latestLocationState.timestamp!)
); );
}); });
const latestLocationEvent = validLocationEvents.sort(sortEventsByLatestContentTimestamp)?.[0]; const latestLocationEvent = validLocationEvents.sort(sortEventsByLatestContentTimestamp)?.[0];
if (latestLocationEvent) { if (latestLocationEvent) {
this._latestLocationEvent = latestLocationEvent; this._latestLocationEvent = latestLocationEvent;
this.emit(BeaconEvent.LocationUpdate, this.latestLocationState); this.emit(BeaconEvent.LocationUpdate, this.latestLocationState!);
} }
} }
private clearLatestLocation = () => { private clearLatestLocation = () => {
this._latestLocationEvent = undefined; this._latestLocationEvent = undefined;
this.emit(BeaconEvent.LocationUpdate, this.latestLocationState); this.emit(BeaconEvent.LocationUpdate, this.latestLocationState!);
}; };
private setBeaconInfo(event: MatrixEvent): void { private setBeaconInfo(event: MatrixEvent): void {
@ -195,9 +196,10 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
// when Alice's system clock deviates slightly from Bob's a beacon Alice intended to be live // when Alice's system clock deviates slightly from Bob's a beacon Alice intended to be live
// may have a start timestamp in the future from Bob's POV // may have a start timestamp in the future from Bob's POV
// handle this by adding 6min of leniency to the start timestamp when it is in the future // handle this by adding 6min of leniency to the start timestamp when it is in the future
const startTimestamp = this._beaconInfo?.timestamp > Date.now() ? if (!this.beaconInfo) return;
this._beaconInfo?.timestamp - 360000 /* 6min */ : const startTimestamp = this.beaconInfo.timestamp > Date.now() ?
this._beaconInfo?.timestamp; this.beaconInfo.timestamp - 360000 /* 6min */ :
this.beaconInfo.timestamp;
this._isLive = !!this._beaconInfo?.live && this._isLive = !!this._beaconInfo?.live &&
isTimestampInDuration(startTimestamp, this._beaconInfo?.timeout, Date.now()); isTimestampInDuration(startTimestamp, this._beaconInfo?.timeout, Date.now());

View File

@ -822,7 +822,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
// linkedlist to see which comes first. // linkedlist to see which comes first.
// first work forwards from timeline1 // first work forwards from timeline1
let tl = timeline1; let tl: EventTimeline | null = timeline1;
while (tl) { while (tl) {
if (tl === timeline2) { if (tl === timeline2) {
// timeline1 is before timeline2 // timeline1 is before timeline2

View File

@ -77,7 +77,7 @@ export class EventTimeline {
event.sender = stateContext.getSentinelMember(event.getSender()); event.sender = stateContext.getSentinelMember(event.getSender());
} }
if (!event.target?.events?.member && event.getType() === EventType.RoomMember) { if (!event.target?.events?.member && event.getType() === EventType.RoomMember) {
event.target = stateContext.getSentinelMember(event.getStateKey()); event.target = stateContext.getSentinelMember(event.getStateKey()!);
} }
if (event.isState()) { if (event.isState()) {
@ -97,8 +97,8 @@ export class EventTimeline {
private baseIndex = 0; private baseIndex = 0;
private startState: RoomState; private startState: RoomState;
private endState: RoomState; private endState: RoomState;
private prevTimeline?: EventTimeline; private prevTimeline: EventTimeline | null = null;
private nextTimeline?: EventTimeline; private nextTimeline: EventTimeline | null = null;
public paginationRequests: Record<Direction, Promise<boolean> | null> = { public paginationRequests: Record<Direction, Promise<boolean> | null> = {
[Direction.Backward]: null, [Direction.Backward]: null,
[Direction.Forward]: null, [Direction.Forward]: null,
@ -131,9 +131,6 @@ export class EventTimeline {
this.endState = new RoomState(this.roomId); this.endState = new RoomState(this.roomId);
this.endState.paginationToken = null; this.endState.paginationToken = null;
this.prevTimeline = null;
this.nextTimeline = null;
// this is used by client.js // this is used by client.js
this.paginationRequests = { 'b': null, 'f': null }; this.paginationRequests = { 'b': null, 'f': null };
@ -226,7 +223,7 @@ export class EventTimeline {
* Get the ID of the room for this timeline * Get the ID of the room for this timeline
* @return {string} room ID * @return {string} room ID
*/ */
public getRoomId(): string { public getRoomId(): string | null {
return this.roomId; return this.roomId;
} }
@ -234,7 +231,7 @@ export class EventTimeline {
* Get the filter for this timeline's timelineSet (if any) * Get the filter for this timeline's timelineSet (if any)
* @return {Filter} filter * @return {Filter} filter
*/ */
public getFilter(): Filter { public getFilter(): Filter | undefined {
return this.eventTimelineSet.getFilter(); return this.eventTimelineSet.getFilter();
} }
@ -324,7 +321,7 @@ export class EventTimeline {
* @return {?EventTimeline} previous or following timeline, if they have been * @return {?EventTimeline} previous or following timeline, if they have been
* joined up. * joined up.
*/ */
public getNeighbouringTimeline(direction: Direction): EventTimeline { public getNeighbouringTimeline(direction: Direction): EventTimeline | null {
if (direction == EventTimeline.BACKWARDS) { if (direction == EventTimeline.BACKWARDS) {
return this.prevTimeline; return this.prevTimeline;
} else if (direction == EventTimeline.FORWARDS) { } else if (direction == EventTimeline.FORWARDS) {
@ -391,7 +388,7 @@ export class EventTimeline {
roomState?: RoomState, roomState?: RoomState,
): void { ): void {
let toStartOfTimeline = !!toStartOfTimelineOrOpts; let toStartOfTimeline = !!toStartOfTimelineOrOpts;
let timelineWasEmpty: boolean; let timelineWasEmpty: boolean | undefined;
if (typeof (toStartOfTimelineOrOpts) === 'object') { if (typeof (toStartOfTimelineOrOpts) === 'object') {
({ toStartOfTimeline, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts); ({ toStartOfTimeline, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts);
} else if (toStartOfTimelineOrOpts !== undefined) { } else if (toStartOfTimelineOrOpts !== undefined) {

View File

@ -34,6 +34,7 @@ import { TypedReEmitter } from '../ReEmitter';
import { MatrixError } from "../http-api"; import { MatrixError } from "../http-api";
import { TypedEventEmitter } from "./typed-event-emitter"; import { TypedEventEmitter } from "./typed-event-emitter";
import { EventStatus } from "./event-status"; import { EventStatus } from "./event-status";
import { DecryptionError } from "../crypto/algorithms";
export { EventStatus } from "./event-status"; export { EventStatus } from "./event-status";
@ -685,14 +686,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* attempt is completed. * attempt is completed.
*/ */
public async attemptDecryption(crypto: Crypto, options: IDecryptOptions = {}): Promise<void> { public async attemptDecryption(crypto: Crypto, options: IDecryptOptions = {}): Promise<void> {
// For backwards compatibility purposes
// The function signature used to be attemptDecryption(crypto, isRetry)
if (typeof options === "boolean") {
options = {
isRetry: options,
};
}
// start with a couple of sanity checks. // start with a couple of sanity checks.
if (!this.isEncrypted()) { if (!this.isEncrypted()) {
throw new Error("Attempt to decrypt event which isn't encrypted"); throw new Error("Attempt to decrypt event which isn't encrypted");
@ -822,15 +815,19 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
// //
if (this.retryDecryption) { if (this.retryDecryption) {
// decryption error, but we have a retry queued. // decryption error, but we have a retry queued.
logger.log(`Got error decrypting event (id=${this.getId()}: ${e.detailedString}), but retrying`, e); logger.log(`Got error decrypting event (id=${this.getId()}: ` +
`${(<DecryptionError>e).detailedString}), but retrying`, e);
continue; continue;
} }
// decryption error, no retries queued. Warn about the error and // decryption error, no retries queued. Warn about the error and
// set it to m.bad.encrypted. // set it to m.bad.encrypted.
logger.warn(`Got error decrypting event (id=${this.getId()}: ${e.detailedString})`, e); logger.warn(
`Got error decrypting event (id=${this.getId()}: ${(<DecryptionError>e).detailedString})`,
e,
);
res = this.badEncryptedMessage(e.message); res = this.badEncryptedMessage((<DecryptionError>e).message);
} }
// at this point, we've either successfully decrypted the event, or have given up // at this point, we've either successfully decrypted the event, or have given up

View File

@ -118,31 +118,31 @@ export class RelationsContainer {
const { event_id: relatesToEventId, rel_type: relationType } = relation; const { event_id: relatesToEventId, rel_type: relationType } = relation;
const eventType = event.getType(); const eventType = event.getType();
let relationsForEvent = this.relations.get(relatesToEventId); let relationsForEvent = this.relations.get(relatesToEventId!);
if (!relationsForEvent) { if (!relationsForEvent) {
relationsForEvent = new Map<RelationType | string, Map<EventType | string, Relations>>(); relationsForEvent = new Map<RelationType | string, Map<EventType | string, Relations>>();
this.relations.set(relatesToEventId, relationsForEvent); this.relations.set(relatesToEventId!, relationsForEvent);
} }
let relationsWithRelType = relationsForEvent.get(relationType); let relationsWithRelType = relationsForEvent.get(relationType!);
if (!relationsWithRelType) { if (!relationsWithRelType) {
relationsWithRelType = new Map<EventType | string, Relations>(); relationsWithRelType = new Map<EventType | string, Relations>();
relationsForEvent.set(relationType, relationsWithRelType); relationsForEvent.set(relationType!, relationsWithRelType);
} }
let relationsWithEventType = relationsWithRelType.get(eventType); let relationsWithEventType = relationsWithRelType.get(eventType);
if (!relationsWithEventType) { if (!relationsWithEventType) {
relationsWithEventType = new Relations( relationsWithEventType = new Relations(
relationType, relationType!,
eventType, eventType,
this.client, this.client,
); );
relationsWithRelType.set(eventType, relationsWithEventType); relationsWithRelType.set(eventType, relationsWithEventType);
const room = this.room ?? timelineSet?.room; const room = this.room ?? timelineSet?.room;
const relatesToEvent = timelineSet?.findEventById(relatesToEventId) const relatesToEvent = timelineSet?.findEventById(relatesToEventId!)
?? room?.findEventById(relatesToEventId) ?? room?.findEventById(relatesToEventId!)
?? room?.getPendingEvent(relatesToEventId); ?? room?.getPendingEvent(relatesToEventId!);
if (relatesToEvent) { if (relatesToEvent) {
relationsWithEventType.setTargetEvent(relatesToEvent); relationsWithEventType.setTargetEvent(relatesToEvent);
} }

View File

@ -47,7 +47,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
private annotationsByKey: Record<string, Set<MatrixEvent>> = {}; private annotationsByKey: Record<string, Set<MatrixEvent>> = {};
private annotationsBySender: Record<string, Set<MatrixEvent>> = {}; private annotationsBySender: Record<string, Set<MatrixEvent>> = {};
private sortedAnnotationsByKey: [string, Set<MatrixEvent>][] = []; private sortedAnnotationsByKey: [string, Set<MatrixEvent>][] = [];
private targetEvent: MatrixEvent = null; private targetEvent: MatrixEvent | null = null;
private creationEmitted = false; private creationEmitted = false;
private readonly client: MatrixClient; private readonly client: MatrixClient;
@ -107,7 +107,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
this.addAnnotationToAggregation(event); this.addAnnotationToAggregation(event);
} else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) { } else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
const lastReplacement = await this.getLastReplacement(); const lastReplacement = await this.getLastReplacement();
this.targetEvent.makeReplaced(lastReplacement); this.targetEvent.makeReplaced(lastReplacement!);
} }
event.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); event.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
@ -148,7 +148,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
this.removeAnnotationFromAggregation(event); this.removeAnnotationFromAggregation(event);
} else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) { } else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
const lastReplacement = await this.getLastReplacement(); const lastReplacement = await this.getLastReplacement();
this.targetEvent.makeReplaced(lastReplacement); this.targetEvent.makeReplaced(lastReplacement!);
} }
this.emit(RelationsEvent.Remove, event); this.emit(RelationsEvent.Remove, event);
@ -189,10 +189,8 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
} }
private addAnnotationToAggregation(event: MatrixEvent): void { private addAnnotationToAggregation(event: MatrixEvent): void {
const { key } = event.getRelation(); const { key } = event.getRelation() ?? {};
if (!key) { if (!key) return;
return;
}
let eventsForKey = this.annotationsByKey[key]; let eventsForKey = this.annotationsByKey[key];
if (!eventsForKey) { if (!eventsForKey) {
@ -218,10 +216,8 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
} }
private removeAnnotationFromAggregation(event: MatrixEvent): void { private removeAnnotationFromAggregation(event: MatrixEvent): void {
const { key } = event.getRelation(); const { key } = event.getRelation() ?? {};
if (!key) { if (!key) return;
return;
}
const eventsForKey = this.annotationsByKey[key]; const eventsForKey = this.annotationsByKey[key];
if (eventsForKey) { if (eventsForKey) {
@ -265,7 +261,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
this.removeAnnotationFromAggregation(redactedEvent); this.removeAnnotationFromAggregation(redactedEvent);
} else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) { } else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
const lastReplacement = await this.getLastReplacement(); const lastReplacement = await this.getLastReplacement();
this.targetEvent.makeReplaced(lastReplacement); this.targetEvent.makeReplaced(lastReplacement!);
} }
redactedEvent.removeListener(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); redactedEvent.removeListener(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
@ -283,7 +279,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
* An array of [key, events] pairs sorted by descending event count. * An array of [key, events] pairs sorted by descending event count.
* The events are stored in a Set (which preserves insertion order). * The events are stored in a Set (which preserves insertion order).
*/ */
public getSortedAnnotationsByKey() { public getSortedAnnotationsByKey(): [string, Set<MatrixEvent>][] | null {
if (this.relationType !== RelationType.Annotation) { if (this.relationType !== RelationType.Annotation) {
// Other relation types are not grouped currently. // Other relation types are not grouped currently.
return null; return null;
@ -301,7 +297,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
* An object with each relation sender as a key and the matching Set of * An object with each relation sender as a key and the matching Set of
* events for that sender as a value. * events for that sender as a value.
*/ */
public getAnnotationsBySender() { public getAnnotationsBySender(): Record<string, Set<MatrixEvent>> | null {
if (this.relationType !== RelationType.Annotation) { if (this.relationType !== RelationType.Annotation) {
// Other relation types are not grouped currently. // Other relation types are not grouped currently.
return null; return null;
@ -335,8 +331,8 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace); const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
const minTs = replaceRelation?.origin_server_ts; const minTs = replaceRelation?.origin_server_ts;
const lastReplacement = this.getRelations().reduce((last, event) => { const lastReplacement = this.getRelations().reduce<MatrixEvent | null>((last, event) => {
if (event.getSender() !== this.targetEvent.getSender()) { if (event.getSender() !== this.targetEvent!.getSender()) {
return last; return last;
} }
if (minTs && minTs > event.getTs()) { if (minTs && minTs > event.getTs()) {
@ -348,8 +344,8 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
return event; return event;
}, null); }, null);
if (lastReplacement?.shouldAttemptDecryption()) { if (lastReplacement?.shouldAttemptDecryption() && this.client.isCryptoEnabled()) {
await lastReplacement.attemptDecryption(this.client.crypto); await lastReplacement.attemptDecryption(this.client.crypto!);
} else if (lastReplacement?.isBeingDecrypted()) { } else if (lastReplacement?.isBeingDecrypted()) {
await lastReplacement.getDecryptionPromise(); await lastReplacement.getDecryptionPromise();
} }

View File

@ -1604,7 +1604,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
// find the earliest unfiltered timeline // find the earliest unfiltered timeline
let timeline = unfilteredLiveTimeline; let timeline = unfilteredLiveTimeline;
while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) { while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) {
timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS); timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)!;
} }
timelineSet.getLiveTimeline().setPaginationToken( timelineSet.getLiveTimeline().setPaginationToken(

View File

@ -83,7 +83,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
private reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>; private reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>;
private lastEvent: MatrixEvent; private lastEvent!: MatrixEvent;
private replyCount = 0; private replyCount = 0;
public readonly room: Room; public readonly room: Room;
@ -185,7 +185,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
this.lastEvent = events.find(e => ( this.lastEvent = events.find(e => (
!e.isRedacted() && !e.isRedacted() &&
e.isRelation(THREAD_RELATION_TYPE.name) e.isRelation(THREAD_RELATION_TYPE.name)
)) ?? this.rootEvent; )) ?? this.rootEvent!;
this.emit(ThreadEvent.Update, this); this.emit(ThreadEvent.Update, this);
}; };
@ -267,7 +267,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
this.client.decryptEventIfNeeded(event, {}); this.client.decryptEventIfNeeded(event, {});
} else if (!toStartOfTimeline && } else if (!toStartOfTimeline &&
this.initialEventsFetched && this.initialEventsFetched &&
event.localTimestamp > this.lastReply()?.localTimestamp event.localTimestamp > this.lastReply()!.localTimestamp
) { ) {
this.fetchEditsWhereNeeded(event); this.fetchEditsWhereNeeded(event);
this.addEventToTimeline(event, false); this.addEventToTimeline(event, false);
@ -289,7 +289,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
} }
} }
private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship { private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship | undefined {
return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name); return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
} }
@ -302,7 +302,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
if (Thread.hasServerSideSupport && bundledRelationship) { if (Thread.hasServerSideSupport && bundledRelationship) {
this.replyCount = bundledRelationship.count; this.replyCount = bundledRelationship.count;
this._currentUserParticipated = bundledRelationship.current_user_participated; this._currentUserParticipated = !!bundledRelationship.current_user_participated;
const event = new MatrixEvent({ const event = new MatrixEvent({
room_id: this.rootEvent.getRoomId(), room_id: this.rootEvent.getRoomId(),
@ -407,7 +407,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
} }
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, dir: Direction.Backward }): Promise<{ public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, dir: Direction.Backward }): Promise<{
originalEvent: MatrixEvent; originalEvent?: MatrixEvent;
events: MatrixEvent[]; events: MatrixEvent[];
nextBatch?: string | null; nextBatch?: string | null;
prevBatch?: string; prevBatch?: string;
@ -427,7 +427,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
// When there's no nextBatch returned with a `from` request we have reached // When there's no nextBatch returned with a `from` request we have reached
// the end of the thread, and therefore want to return an empty one // the end of the thread, and therefore want to return an empty one
if (!opts.to && !nextBatch) { if (!opts.to && !nextBatch && originalEvent) {
events = [...events, originalEvent]; events = [...events, originalEvent];
} }

View File

@ -184,7 +184,7 @@ export class PushProcessor {
private static cachedGlobToRegex: Record<string, RegExp> = {}; // $glob: RegExp private static cachedGlobToRegex: Record<string, RegExp> = {}; // $glob: RegExp
private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule { private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule | null {
for (let ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) { for (let ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) {
const kind = RULEKINDS_IN_ORDER[ruleKindIndex]; const kind = RULEKINDS_IN_ORDER[ruleKindIndex];
const ruleset = kindset[kind]; const ruleset = kindset[kind];
@ -338,19 +338,19 @@ export class PushProcessor {
private eventFulfillsDisplayNameCondition(cond: IContainsDisplayNameCondition, ev: MatrixEvent): boolean { private eventFulfillsDisplayNameCondition(cond: IContainsDisplayNameCondition, ev: MatrixEvent): boolean {
let content = ev.getContent(); let content = ev.getContent();
if (ev.isEncrypted() && ev.getClearContent()) { if (ev.isEncrypted() && ev.getClearContent()) {
content = ev.getClearContent(); content = ev.getClearContent()!;
} }
if (!content || !content.body || typeof content.body != 'string') { if (!content || !content.body || typeof content.body != 'string') {
return false; return false;
} }
const room = this.client.getRoom(ev.getRoomId()); const room = this.client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members || const member = room?.currentState?.getMember(this.client.credentials.userId!);
!room.currentState.getMember(this.client.credentials.userId)) { if (!member) {
return false; return false;
} }
const displayName = room.currentState.getMember(this.client.credentials.userId).name; const displayName = member.name;
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay // N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
// as shorthand for [^0-9A-Za-z_]. // as shorthand for [^0-9A-Za-z_].
@ -396,7 +396,7 @@ export class PushProcessor {
private valueForDottedKey(key: string, ev: MatrixEvent): any { private valueForDottedKey(key: string, ev: MatrixEvent): any {
const parts = key.split('.'); const parts = key.split('.');
let val; let val: any;
// special-case the first component to deal with encrypted messages // special-case the first component to deal with encrypted messages
const firstPart = parts[0]; const firstPart = parts[0];
@ -412,7 +412,7 @@ export class PushProcessor {
} }
while (parts.length > 0) { while (parts.length > 0) {
const thisPart = parts.shift(); const thisPart = parts.shift()!;
if (isNullOrUndefined(val[thisPart])) { if (isNullOrUndefined(val[thisPart])) {
return null; return null;
} }
@ -421,7 +421,7 @@ export class PushProcessor {
return val; return val;
} }
private matchingRuleForEventWithRulesets(ev: MatrixEvent, rulesets): IAnnotatedPushRule { private matchingRuleForEventWithRulesets(ev: MatrixEvent, rulesets?: IPushRules): IAnnotatedPushRule | null {
if (!rulesets) { if (!rulesets) {
return null; return null;
} }
@ -432,7 +432,7 @@ export class PushProcessor {
return this.matchingRuleFromKindSet(ev, rulesets.global); return this.matchingRuleFromKindSet(ev, rulesets.global);
} }
private pushActionsForEventAndRulesets(ev: MatrixEvent, rulesets): IActionsObject { private pushActionsForEventAndRulesets(ev: MatrixEvent, rulesets?: IPushRules): IActionsObject {
const rule = this.matchingRuleForEventWithRulesets(ev, rulesets); const rule = this.matchingRuleForEventWithRulesets(ev, rulesets);
if (!rule) { if (!rule) {
return {} as IActionsObject; return {} as IActionsObject;
@ -504,9 +504,9 @@ export class PushProcessor {
* @param {string} ruleId The ID of the rule to search for * @param {string} ruleId The ID of the rule to search for
* @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 { public getPushRuleById(ruleId: string): IPushRule | null {
for (const scope of ['global']) { for (const scope of ['global']) {
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;

View File

@ -36,14 +36,16 @@ let count = 0;
// the key for our callback with the real global.setTimeout // the key for our callback with the real global.setTimeout
let realCallbackKey: NodeJS.Timeout | number; let realCallbackKey: NodeJS.Timeout | number;
// a sorted list of the callbacks to be run. type Callback = {
// each is an object with keys [runAt, func, params, key].
const callbackList: {
runAt: number; runAt: number;
func: (...params: any[]) => void; func: (...params: any[]) => void;
params: any[]; params: any[];
key: number; key: number;
}[] = []; };
// a sorted list of the callbacks to be run.
// each is an object with keys [runAt, func, params, key].
const callbackList: Callback[] = [];
// var debuglog = logger.log.bind(logger); // var debuglog = logger.log.bind(logger);
const debuglog = function(...params: any[]) {}; const debuglog = function(...params: any[]) {};
@ -135,19 +137,19 @@ function scheduleRealCallback(): void {
} }
function runCallbacks(): void { function runCallbacks(): void {
let cb; let cb: Callback;
const timestamp = Date.now(); const timestamp = Date.now();
debuglog("runCallbacks: now:", timestamp); debuglog("runCallbacks: now:", timestamp);
// get the list of things to call // get the list of things to call
const callbacksToRun = []; const callbacksToRun: Callback[] = [];
// eslint-disable-next-line // eslint-disable-next-line
while (true) { while (true) {
const first = callbackList[0]; const first = callbackList[0];
if (!first || first.runAt > timestamp) { if (!first || first.runAt > timestamp) {
break; break;
} }
cb = callbackList.shift(); cb = callbackList.shift()!;
debuglog("runCallbacks: popping", cb.key); debuglog("runCallbacks: popping", cb.key);
callbacksToRun.push(cb); callbacksToRun.push(cb);
} }
@ -162,8 +164,7 @@ function runCallbacks(): void {
try { try {
cb.func.apply(global, cb.params); cb.func.apply(global, cb.params);
} catch (e) { } catch (e) {
logger.error("Uncaught exception in callback function", logger.error("Uncaught exception in callback function", e);
e.stack || e);
} }
} }
} }

View File

@ -203,13 +203,13 @@ export class MSC3906Rendezvous {
true, false, true, true, false, true,
); );
const masterPublicKey = this.client.crypto.crossSigningInfo.getId('master'); const masterPublicKey = this.client.crypto.crossSigningInfo.getId('master')!;
await this.send({ await this.send({
type: PayloadType.Finish, type: PayloadType.Finish,
outcome: Outcome.Verified, outcome: Outcome.Verified,
verifying_device_id: this.client.getDeviceId(), verifying_device_id: this.client.getDeviceId(),
verifying_device_key: this.client.getDeviceEd25519Key(), verifying_device_key: this.client.getDeviceEd25519Key()!,
master_key: masterPublicKey, master_key: masterPublicKey,
}); });

View File

@ -22,6 +22,7 @@ import { Room } from "./models/room";
import { IHierarchyRoom, IHierarchyRelation } from "./@types/spaces"; import { IHierarchyRoom, IHierarchyRelation } from "./@types/spaces";
import { MatrixClient } from "./client"; import { MatrixClient } from "./client";
import { EventType } from "./@types/event"; import { EventType } from "./@types/event";
import { MatrixError } from "./http-api";
export class RoomHierarchy { export class RoomHierarchy {
// Map from room id to list of servers which are listed as a via somewhere in the loaded hierarchy // Map from room id to list of servers which are listed as a via somewhere in the loaded hierarchy
@ -30,7 +31,7 @@ export class RoomHierarchy {
public readonly backRefs = new Map<string, string[]>(); public readonly backRefs = new Map<string, string[]>();
// Map from room id to object // Map from room id to object
public readonly roomMap = new Map<string, IHierarchyRoom>(); public readonly roomMap = new Map<string, IHierarchyRoom>();
private loadRequest: ReturnType<MatrixClient["getRoomHierarchy"]>; private loadRequest?: ReturnType<MatrixClient["getRoomHierarchy"]>;
private nextBatch?: string; private nextBatch?: string;
private _rooms?: IHierarchyRoom[]; private _rooms?: IHierarchyRoom[];
private serverSupportError?: Error; private serverSupportError?: Error;
@ -65,7 +66,7 @@ export class RoomHierarchy {
return !!this.loadRequest; return !!this.loadRequest;
} }
public get rooms(): IHierarchyRoom[] { public get rooms(): IHierarchyRoom[] | undefined {
return this._rooms; return this._rooms;
} }
@ -84,15 +85,15 @@ export class RoomHierarchy {
try { try {
({ rooms, next_batch: this.nextBatch } = await this.loadRequest); ({ rooms, next_batch: this.nextBatch } = await this.loadRequest);
} catch (e) { } catch (e) {
if (e.errcode === "M_UNRECOGNIZED") { if ((<MatrixError>e).errcode === "M_UNRECOGNIZED") {
this.serverSupportError = e; this.serverSupportError = <MatrixError>e;
} else { } else {
throw e; throw e;
} }
return []; return [];
} finally { } finally {
this.loadRequest = null; this.loadRequest = undefined;
} }
if (this._rooms) { if (this._rooms) {
@ -112,14 +113,14 @@ export class RoomHierarchy {
if (!this.backRefs.has(childRoomId)) { if (!this.backRefs.has(childRoomId)) {
this.backRefs.set(childRoomId, []); this.backRefs.set(childRoomId, []);
} }
this.backRefs.get(childRoomId).push(room.room_id); this.backRefs.get(childRoomId)!.push(room.room_id);
// fill viaMap // fill viaMap
if (Array.isArray(ev.content.via)) { if (Array.isArray(ev.content.via)) {
if (!this.viaMap.has(childRoomId)) { if (!this.viaMap.has(childRoomId)) {
this.viaMap.set(childRoomId, new Set()); this.viaMap.set(childRoomId, new Set());
} }
const vias = this.viaMap.get(childRoomId); const vias = this.viaMap.get(childRoomId)!;
ev.content.via.forEach(via => vias.add(via)); ev.content.via.forEach(via => vias.add(via));
} }
}); });
@ -128,11 +129,11 @@ export class RoomHierarchy {
return rooms; return rooms;
} }
public getRelation(parentId: string, childId: string): IHierarchyRelation { public getRelation(parentId: string, childId: string): IHierarchyRelation | undefined {
return this.roomMap.get(parentId)?.children_state.find(e => e.state_key === childId); return this.roomMap.get(parentId)?.children_state.find(e => e.state_key === childId);
} }
public isSuggested(parentId: string, childId: string): boolean { public isSuggested(parentId: string, childId: string): boolean | undefined {
return this.getRelation(parentId, childId)?.content.suggested; return this.getRelation(parentId, childId)?.content.suggested;
} }

View File

@ -63,7 +63,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
* @see module:scheduler~retryAlgorithm * @see module:scheduler~retryAlgorithm
*/ */
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent, attempts: number, err: MatrixError): number { public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent | null, attempts: number, err: MatrixError): number {
if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) { if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) {
// client error; no amount of retrying with save you now. // client error; no amount of retrying with save you now.
return -1; return -1;
@ -114,7 +114,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
// }, ...] // }, ...]
private readonly queues: Record<string, IQueueEntry<T>[]> = {}; private readonly queues: Record<string, IQueueEntry<T>[]> = {};
private activeQueues: string[] = []; private activeQueues: string[] = [];
private procFn: ProcessFunction<T> = null; private procFn: ProcessFunction<T> | null = null;
constructor( constructor(
public readonly retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT, public readonly retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT,
@ -130,7 +130,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
* this array <i>will</i> modify the underlying event in the queue. * this array <i>will</i> modify the underlying event in the queue.
* @see MatrixScheduler.removeEventFromQueue To remove an event from the queue. * @see MatrixScheduler.removeEventFromQueue To remove an event from the queue.
*/ */
public getQueueForEvent(event: MatrixEvent): MatrixEvent[] { public getQueueForEvent(event: MatrixEvent): MatrixEvent[] | null {
const name = this.queueAlgorithm(event); const name = this.queueAlgorithm(event);
if (!name || !this.queues[name]) { if (!name || !this.queues[name]) {
return null; return null;
@ -159,6 +159,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
removed = true; removed = true;
return true; return true;
} }
return false;
}); });
return removed; return removed;
} }
@ -239,7 +240,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
// This way enqueued relations/redactions to enqueued events can receive // This way enqueued relations/redactions to enqueued events can receive
// the remove id of their target before being sent. // the remove id of their target before being sent.
Promise.resolve().then(() => { Promise.resolve().then(() => {
return this.procFn(obj.event); return this.procFn!(obj.event);
}).then((res) => { }).then((res) => {
// remove this from the queue // remove this from the queue
this.removeNextEvent(queueName); this.removeNextEvent(queueName);
@ -265,18 +266,18 @@ export class MatrixScheduler<T = ISendEventResponse> {
}); });
}; };
private peekNextEvent(queueName: string): IQueueEntry<T> { private peekNextEvent(queueName: string): IQueueEntry<T> | undefined {
const queue = this.queues[queueName]; const queue = this.queues[queueName];
if (!Array.isArray(queue)) { if (!Array.isArray(queue)) {
return null; return undefined;
} }
return queue[0]; return queue[0];
} }
private removeNextEvent(queueName: string): IQueueEntry<T> { private removeNextEvent(queueName: string): IQueueEntry<T> | undefined {
const queue = this.queues[queueName]; const queue = this.queues[queueName];
if (!Array.isArray(queue)) { if (!Array.isArray(queue)) {
return null; return undefined;
} }
return queue.shift(); return queue.shift();
} }

View File

@ -52,7 +52,7 @@ class ExtensionE2EE implements Extension {
return ExtensionState.PreProcess; return ExtensionState.PreProcess;
} }
public onRequest(isInitial: boolean): object { public onRequest(isInitial: boolean): object | undefined {
if (!isInitial) { if (!isInitial) {
return undefined; return undefined;
} }
@ -90,7 +90,7 @@ class ExtensionE2EE implements Extension {
} }
class ExtensionToDevice implements Extension { class ExtensionToDevice implements Extension {
private nextBatch?: string = null; private nextBatch: string | null = null;
constructor(private readonly client: MatrixClient) {} constructor(private readonly client: MatrixClient) {}
@ -114,7 +114,7 @@ class ExtensionToDevice implements Extension {
} }
public async onResponse(data: object): Promise<void> { public async onResponse(data: object): Promise<void> {
const cancelledKeyVerificationTxns = []; const cancelledKeyVerificationTxns: string[] = [];
data["events"] = data["events"] || []; data["events"] = data["events"] || [];
data["events"] data["events"]
.map(this.client.getEventMapper()) .map(this.client.getEventMapper())
@ -125,7 +125,7 @@ class ExtensionToDevice implements Extension {
// so we can flag the verification events as cancelled in the loop // so we can flag the verification events as cancelled in the loop
// below. // below.
if (toDeviceEvent.getType() === "m.key.verification.cancel") { if (toDeviceEvent.getType() === "m.key.verification.cancel") {
const txnId = toDeviceEvent.getContent()['transaction_id']; const txnId: string | undefined = toDeviceEvent.getContent()['transaction_id'];
if (txnId) { if (txnId) {
cancelledKeyVerificationTxns.push(txnId); cancelledKeyVerificationTxns.push(txnId);
} }
@ -177,7 +177,7 @@ class ExtensionAccountData implements Extension {
return ExtensionState.PostProcess; return ExtensionState.PostProcess;
} }
public onRequest(isInitial: boolean): object { public onRequest(isInitial: boolean): object | undefined {
if (!isInitial) { if (!isInitial) {
return undefined; return undefined;
} }
@ -235,9 +235,9 @@ class ExtensionAccountData implements Extension {
* sliding sync API, see sliding-sync.ts or the class SlidingSync. * sliding sync API, see sliding-sync.ts or the class SlidingSync.
*/ */
export class SlidingSyncSdk { export class SlidingSyncSdk {
private syncState: SyncState = null; private syncState: SyncState | null = null;
private syncStateData: ISyncStateData; private syncStateData?: ISyncStateData;
private lastPos: string = null; private lastPos: string | null = null;
private failCount = 0; private failCount = 0;
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
@ -259,7 +259,7 @@ export class SlidingSyncSdk {
} }
if (client.getNotifTimelineSet()) { if (client.getNotifTimelineSet()) {
client.reEmitter.reEmit(client.getNotifTimelineSet(), [ client.reEmitter.reEmit(client.getNotifTimelineSet()!, [
RoomEvent.Timeline, RoomEvent.Timeline,
RoomEvent.TimelineReset, RoomEvent.TimelineReset,
]); ]);
@ -293,7 +293,7 @@ export class SlidingSyncSdk {
this.processRoomData(this.client, room, roomData); this.processRoomData(this.client, room, roomData);
} }
private onLifecycle(state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err: Error | null): void { private onLifecycle(state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err?: Error): void {
if (err) { if (err) {
logger.debug("onLifecycle", state, err); logger.debug("onLifecycle", state, err);
} }
@ -306,7 +306,7 @@ export class SlidingSyncSdk {
// Element won't stop showing the initial loading spinner unless we fire SyncState.Prepared // Element won't stop showing the initial loading spinner unless we fire SyncState.Prepared
if (!this.lastPos) { if (!this.lastPos) {
this.updateSyncState(SyncState.Prepared, { this.updateSyncState(SyncState.Prepared, {
oldSyncToken: this.lastPos, oldSyncToken: undefined,
nextSyncToken: resp.pos, nextSyncToken: resp.pos,
catchingUp: false, catchingUp: false,
fromCache: false, fromCache: false,
@ -315,7 +315,7 @@ export class SlidingSyncSdk {
// Conversely, Element won't show the room list unless there is at least 1x SyncState.Syncing // Conversely, Element won't show the room list unless there is at least 1x SyncState.Syncing
// so hence for the very first sync we will fire prepared then immediately syncing. // so hence for the very first sync we will fire prepared then immediately syncing.
this.updateSyncState(SyncState.Syncing, { this.updateSyncState(SyncState.Syncing, {
oldSyncToken: this.lastPos, oldSyncToken: this.lastPos!,
nextSyncToken: resp.pos, nextSyncToken: resp.pos,
catchingUp: false, catchingUp: false,
fromCache: false, fromCache: false,
@ -357,7 +357,7 @@ export class SlidingSyncSdk {
* store. * store.
*/ */
public async peek(_roomId: string): Promise<Room> { public async peek(_roomId: string): Promise<Room> {
return null; // TODO return null!; // TODO
} }
/** /**
@ -373,7 +373,7 @@ export class SlidingSyncSdk {
* @see module:client~MatrixClient#event:"sync" * @see module:client~MatrixClient#event:"sync"
* @return {?String} * @return {?String}
*/ */
public getSyncState(): SyncState { public getSyncState(): SyncState | null {
return this.syncState; return this.syncState;
} }
@ -385,8 +385,8 @@ export class SlidingSyncSdk {
* this object. * this object.
* @return {?Object} * @return {?Object}
*/ */
public getSyncStateData(): ISyncStateData { public getSyncStateData(): ISyncStateData | null {
return this.syncStateData; return this.syncStateData ?? null;
} }
private shouldAbortSync(error: MatrixError): boolean { private shouldAbortSync(error: MatrixError): boolean {
@ -500,8 +500,7 @@ export class SlidingSyncSdk {
if (roomData.initial) { if (roomData.initial) {
// set the back-pagination token. Do this *before* adding any // set the back-pagination token. Do this *before* adding any
// events so that clients can start back-paginating. // events so that clients can start back-paginating.
room.getLiveTimeline().setPaginationToken( room.getLiveTimeline().setPaginationToken(roomData.prev_batch ?? null, EventTimeline.BACKWARDS);
roomData.prev_batch, EventTimeline.BACKWARDS);
} }
/* TODO /* TODO
@ -687,7 +686,7 @@ export class SlidingSyncSdk {
// slightly naughty by doctoring the invite event but this means all // slightly naughty by doctoring the invite event but this means all
// the code paths remain the same between invite/join display name stuff // the code paths remain the same between invite/join display name stuff
// which is a worthy trade-off for some minor pollution. // which is a worthy trade-off for some minor pollution.
const inviteEvent = member.events.member; const inviteEvent = member.events.member!;
if (inviteEvent.getContent().membership !== "invite") { if (inviteEvent.getContent().membership !== "invite") {
// between resolving and now they have since joined, so don't clobber // between resolving and now they have since joined, so don't clobber
return; return;
@ -723,7 +722,7 @@ export class SlidingSyncSdk {
break; break;
} catch (err) { } catch (err) {
logger.error("Getting push rules failed", err); logger.error("Getting push rules failed", err);
if (this.shouldAbortSync(err)) { if (this.shouldAbortSync(<MatrixError>err)) {
return; return;
} }
} }
@ -787,7 +786,7 @@ export class SlidingSyncSdk {
return a.getTs() - b.getTs(); return a.getTs() - b.getTs();
}); });
this.notifEvents.forEach((event) => { this.notifEvents.forEach((event) => {
this.client.getNotifTimelineSet().addLiveEvent(event); this.client.getNotifTimelineSet()?.addLiveEvent(event);
}); });
this.notifEvents = []; this.notifEvents = [];
} }
@ -815,7 +814,7 @@ function ensureNameEvent(client: MatrixClient, roomId: string, roomData: MSC3575
content: { content: {
name: roomData.name, name: roomData.name,
}, },
sender: client.getUserId(), sender: client.getUserId()!,
origin_server_ts: new Date().getTime(), origin_server_ts: new Date().getTime(),
}); });
return roomData; return roomData;
@ -824,7 +823,7 @@ function ensureNameEvent(client: MatrixClient, roomId: string, roomData: MSC3575
// 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, 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 Array<IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent>).map(function(e) {
e["room_id"] = roomId; e["room_id"] = roomId;

View File

@ -19,6 +19,7 @@ import { MatrixClient } from "./client";
import { IRoomEvent, IStateEvent } from "./sync-accumulator"; import { IRoomEvent, IStateEvent } from "./sync-accumulator";
import { TypedEventEmitter } from "./models/typed-event-emitter"; import { TypedEventEmitter } from "./models/typed-event-emitter";
import { sleep, IDeferred, defer } from "./utils"; import { sleep, IDeferred, defer } from "./utils";
import { HTTPError } from "./http-api";
// /sync requests allow you to set a timeout= but the request may continue // /sync requests allow you to set a timeout= but the request may continue
// beyond that and wedge forever, so we need to track how long we are willing // beyond that and wedge forever, so we need to track how long we are willing
@ -153,12 +154,12 @@ export enum SlidingSyncState {
* multiple sliding windows, and maintains the index->room_id mapping. * multiple sliding windows, and maintains the index->room_id mapping.
*/ */
class SlidingList { class SlidingList {
private list: MSC3575List; private list!: MSC3575List;
private isModified: boolean; private isModified?: boolean;
// returned data // returned data
public roomIndexToRoomId: Record<number, string>; public roomIndexToRoomId: Record<number, string> = {};
public joinedCount: number; public joinedCount = 0;
/** /**
* Construct a new sliding list. * Construct a new sliding list.
@ -271,7 +272,7 @@ 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; onRequest(isInitial: boolean): object | 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.
@ -322,7 +323,7 @@ export enum SlidingSyncEvent {
export type SlidingSyncEventHandlerMap = { export type SlidingSyncEventHandlerMap = {
[SlidingSyncEvent.RoomData]: (roomId: string, roomData: MSC3575RoomData) => void; [SlidingSyncEvent.RoomData]: (roomId: string, roomData: MSC3575RoomData) => void;
[SlidingSyncEvent.Lifecycle]: ( [SlidingSyncEvent.Lifecycle]: (
state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err: Error | null, state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err?: Error,
) => void; ) => void;
[SlidingSyncEvent.List]: ( [SlidingSyncEvent.List]: (
listIndex: number, joinedCount: number, roomIndexToRoomId: Record<number, string>, listIndex: number, joinedCount: number, roomIndexToRoomId: Record<number, string>,
@ -342,7 +343,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
// flag set when resend() is called because we cannot rely on detecting AbortError in JS SDK :( // flag set when resend() is called because we cannot rely on detecting AbortError in JS SDK :(
private needsResend = false; private needsResend = false;
// the txn_id to send with the next request. // the txn_id to send with the next request.
private txnId?: string = null; private txnId: string | null = null;
// a list (in chronological order of when they were sent) of objects containing the txn ID and // a list (in chronological order of when they were sent) of objects containing the txn ID and
// 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})[] = [];
@ -387,7 +388,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* @param index The list index * @param index The list index
* @returns The list data which contains the rooms in this list * @returns The list data which contains the rooms in this list
*/ */
public getListData(index: number): {joinedCount: number, roomIndexToRoomId: Record<number, string>} { public getListData(index: number): {joinedCount: number, roomIndexToRoomId: Record<number, string>} | null {
if (!this.lists[index]) { if (!this.lists[index]) {
return null; return null;
} }
@ -403,7 +404,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* @param index The list index to get the list for. * @param index The list index to get the list for.
* @returns A copy of the list or undefined. * @returns A copy of the list or undefined.
*/ */
public getList(index: number): MSC3575List { public getList(index: number): MSC3575List | null {
if (!this.lists[index]) { if (!this.lists[index]) {
return null; return null;
} }
@ -532,7 +533,11 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* @param {object} resp The raw sync response JSON * @param {object} resp The raw sync response JSON
* @param {Error?} err Any error that occurred when making the request e.g. network errors. * @param {Error?} err Any error that occurred when making the request e.g. network errors.
*/ */
private invokeLifecycleListeners(state: SlidingSyncState, resp: MSC3575SlidingSyncResponse, err?: Error): void { private invokeLifecycleListeners(
state: SlidingSyncState,
resp: MSC3575SlidingSyncResponse | null,
err?: Error,
): void {
this.emit(SlidingSyncEvent.Lifecycle, state, resp, err); this.emit(SlidingSyncEvent.Lifecycle, state, resp, err);
} }
@ -772,11 +777,11 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
public async start() { public async start() {
this.abortController = new AbortController(); this.abortController = new AbortController();
let currentPos: string; let currentPos: string | undefined;
while (!this.terminated) { while (!this.terminated) {
this.needsResend = false; this.needsResend = false;
let doNotUpdateList = false; let doNotUpdateList = false;
let resp: MSC3575SlidingSyncResponse; let resp: MSC3575SlidingSyncResponse | undefined;
try { try {
const listModifiedCount = this.listModifiedCount; const listModifiedCount = this.listModifiedCount;
const reqBody: MSC3575SlidingSyncRequest = { const reqBody: MSC3575SlidingSyncRequest = {
@ -837,13 +842,13 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
resp, resp,
); );
} catch (err) { } catch (err) {
if (err.httpStatus) { if ((<HTTPError>err).httpStatus) {
this.invokeLifecycleListeners( this.invokeLifecycleListeners(
SlidingSyncState.RequestFinished, SlidingSyncState.RequestFinished,
null, null,
err, <Error>err,
); );
if (err.httpStatus === 400) { if ((<HTTPError>err).httpStatus === 400) {
// session probably expired TODO: assign an errcode // session probably expired TODO: assign an errcode
// so drop state and re-request // so drop state and re-request
this.resetup(); this.resetup();
@ -851,7 +856,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
await sleep(50); // in case the 400 was for something else; don't tightloop await sleep(50); // in case the 400 was for something else; don't tightloop
continue; continue;
} // else fallthrough to generic error handling } // else fallthrough to generic error handling
} else if (this.needsResend || err.name === "AbortError") { } else if (this.needsResend || (<Error>err).name === "AbortError") {
continue; // don't sleep as we caused this error by abort()ing the request. continue; // don't sleep as we caused this error by abort()ing the request.
} }
logger.error(err); logger.error(err);
@ -865,7 +870,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
Object.keys(resp.rooms).forEach((roomId) => { Object.keys(resp.rooms).forEach((roomId) => {
this.invokeRoomDataListeners( this.invokeRoomDataListeners(
roomId, roomId,
resp.rooms[roomId], resp!.rooms[roomId],
); );
}); });

View File

@ -150,7 +150,7 @@ export interface IStore {
* @param {string} filterName * @param {string} filterName
* @param {string} filterId * @param {string} filterId
*/ */
setFilterIdByName(filterName: string, filterId: string): void; setFilterIdByName(filterName: string, filterId?: string): void;
/** /**
* Store user-scoped account data events * Store user-scoped account data events

View File

@ -274,14 +274,14 @@ export class MemoryStore implements IStore {
* @param {string} filterName * @param {string} filterName
* @param {string} filterId * @param {string} filterId
*/ */
public setFilterIdByName(filterName: string, filterId: string) { public setFilterIdByName(filterName: string, filterId?: string) {
if (!this.localStorage) { if (!this.localStorage) {
return; return;
} }
const key = "mxjssdk_memory_filter_" + filterName; const key = "mxjssdk_memory_filter_" + filterName;
try { try {
if (isValidFilterId(filterId)) { if (isValidFilterId(filterId)) {
this.localStorage.setItem(key, filterId); this.localStorage.setItem(key, filterId!);
} else { } else {
this.localStorage.removeItem(key); this.localStorage.removeItem(key);
} }

View File

@ -36,7 +36,7 @@ import { IndexedToDeviceBatch, ToDeviceBatch } from "../models/ToDeviceMessage";
*/ */
export class StubStore implements IStore { export class StubStore implements IStore {
public readonly accountData = {}; // stub public readonly accountData = {}; // stub
private fromToken: string = null; private fromToken: string | null = null;
/** @return {Promise<boolean>} whether or not the database was newly created in this session. */ /** @return {Promise<boolean>} whether or not the database was newly created in this session. */
public isNewlyCreated(): Promise<boolean> { public isNewlyCreated(): Promise<boolean> {
@ -170,7 +170,7 @@ export class StubStore implements IStore {
* @param {string} filterName * @param {string} filterName
* @param {string} filterId * @param {string} filterId
*/ */
public setFilterIdByName(filterName: string, filterId: string) {} public setFilterIdByName(filterName: string, filterId?: string) {}
/** /**
* Store user-scoped account data events * Store user-scoped account data events
@ -223,7 +223,7 @@ export class StubStore implements IStore {
* client state to where it was at the last save, or null if there * client state to where it was at the last save, or null if there
* is no saved sync data. * is no saved sync data.
*/ */
public getSavedSync(): Promise<ISavedSync> { public getSavedSync(): Promise<ISavedSync | null> {
return Promise.resolve(null); return Promise.resolve(null);
} }
@ -244,7 +244,7 @@ export class StubStore implements IStore {
return Promise.resolve(); return Promise.resolve();
} }
public getOutOfBandMembers(): Promise<IStateEventWithRoomId[]> { public getOutOfBandMembers(): Promise<IStateEventWithRoomId[] | null> {
return Promise.resolve(null); return Promise.resolve(null);
} }

View File

@ -191,7 +191,7 @@ export class SyncAccumulator {
// accumulated. We remember this so that any caller can obtain a // accumulated. We remember this so that any caller can obtain a
// coherent /sync response and know at what point they should be // coherent /sync response and know at what point they should be
// streaming from without losing events. // streaming from without losing events.
private nextBatch: string = null; private nextBatch: string | null = null;
/** /**
* @param {Object} opts * @param {Object} opts
@ -384,8 +384,8 @@ export class SyncAccumulator {
if (data.unread_notifications) { if (data.unread_notifications) {
currentData._unreadNotifications = data.unread_notifications; currentData._unreadNotifications = data.unread_notifications;
} }
currentData._unreadThreadNotifications = data[UNREAD_THREAD_NOTIFICATIONS.stable] currentData._unreadThreadNotifications = data[UNREAD_THREAD_NOTIFICATIONS.stable!]
?? data[UNREAD_THREAD_NOTIFICATIONS.unstable] ?? data[UNREAD_THREAD_NOTIFICATIONS.unstable!]
?? undefined; ?? undefined;
if (data.summary) { if (data.summary) {
@ -429,7 +429,7 @@ export class SyncAccumulator {
Object.entries(e.content[eventId]).forEach(([key, value]) => { Object.entries(e.content[eventId]).forEach(([key, value]) => {
if (!isSupportedReceiptType(key)) return; if (!isSupportedReceiptType(key)) return;
Object.keys(value).forEach((userId) => { Object.keys(value!).forEach((userId) => {
// clobber on user ID // clobber on user ID
currentData._readReceipts[userId] = { currentData._readReceipts[userId] = {
data: e.content[eventId][key][userId], data: e.content[eventId][key][userId],
@ -477,16 +477,16 @@ export class SyncAccumulator {
currentData._timeline.push({ currentData._timeline.push({
event: transformedEvent, event: transformedEvent,
token: index === 0 ? data.timeline.prev_batch : 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.
if (currentData._timeline.length > this.opts.maxTimelineEntries) { if (currentData._timeline.length > this.opts.maxTimelineEntries!) {
const startIndex = ( const startIndex = (
currentData._timeline.length - this.opts.maxTimelineEntries currentData._timeline.length - this.opts.maxTimelineEntries!
); );
for (let i = startIndex; i < currentData._timeline.length; i++) { for (let i = startIndex; i < currentData._timeline.length; i++) {
if (currentData._timeline[i].token) { if (currentData._timeline[i].token) {
@ -657,14 +657,14 @@ export class SyncAccumulator {
}); });
return { return {
nextBatch: this.nextBatch, nextBatch: this.nextBatch!,
roomsData: data, roomsData: data,
accountData: accData, accountData: accData,
}; };
} }
public getNextBatchToken(): string { public getNextBatchToken(): string {
return this.nextBatch; return this.nextBatch!;
} }
} }

View File

@ -33,7 +33,7 @@ import { Filter } from "./filter";
import { EventTimeline } from "./models/event-timeline"; import { EventTimeline } from "./models/event-timeline";
import { PushProcessor } from "./pushprocessor"; import { PushProcessor } from "./pushprocessor";
import { logger } from './logger'; import { logger } from './logger';
import { InvalidStoreError } from './errors'; import { InvalidStoreError, InvalidStoreState } from './errors';
import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client"; import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
import { import {
IEphemeral, IEphemeral,
@ -117,7 +117,7 @@ interface ISyncOptions {
} }
export interface ISyncStateData { export interface ISyncStateData {
error?: MatrixError; error?: Error;
oldSyncToken?: string; oldSyncToken?: string;
nextSyncToken?: string; nextSyncToken?: string;
catchingUp?: boolean; catchingUp?: boolean;
@ -163,14 +163,14 @@ type WrappedRoom<T> = T & {
*/ */
export class SyncApi { export class SyncApi {
private _peekRoom: Optional<Room> = null; private _peekRoom: Optional<Room> = null;
private currentSyncRequest: Optional<Promise<ISyncResponse>> = null; private currentSyncRequest?: Promise<ISyncResponse>;
private abortController?: AbortController; private abortController?: AbortController;
private syncState: Optional<SyncState> = null; private syncState: SyncState | null = null;
private syncStateData: Optional<ISyncStateData> = null; // additional data (eg. error object for failed sync) private syncStateData?: ISyncStateData; // additional data (eg. error object for failed sync)
private catchingUp = false; private catchingUp = false;
private running = false; private running = false;
private keepAliveTimer: Optional<ReturnType<typeof setTimeout>> = null; private keepAliveTimer?: ReturnType<typeof setTimeout>;
private connectionReturnedDefer: Optional<IDeferred<boolean>> = null; private connectionReturnedDefer?: IDeferred<boolean>;
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
private failedSyncCount = 0; // Number of consecutive failed /sync requests private failedSyncCount = 0; // Number of consecutive failed /sync requests
private storeIsInvalid = false; // flag set if the store needs to be cleared before we can start private storeIsInvalid = false; // flag set if the store needs to be cleared before we can start
@ -189,7 +189,7 @@ export class SyncApi {
} }
if (client.getNotifTimelineSet()) { if (client.getNotifTimelineSet()) {
client.reEmitter.reEmit(client.getNotifTimelineSet(), [ client.reEmitter.reEmit(client.getNotifTimelineSet()!, [
RoomEvent.Timeline, RoomEvent.Timeline,
RoomEvent.TimelineReset, RoomEvent.TimelineReset,
]); ]);
@ -281,7 +281,7 @@ export class SyncApi {
* Sync rooms the user has left. * Sync rooms the user has left.
* @return {Promise} Resolved when they've been added to the store. * @return {Promise} Resolved when they've been added to the store.
*/ */
public syncLeftRooms() { public async syncLeftRooms(): Promise<Room[]> {
const client = this.client; const client = this.client;
// grab a filter with limit=1 and include_leave=true // grab a filter with limit=1 and include_leave=true
@ -289,55 +289,62 @@ export class SyncApi {
filter.setTimelineLimit(1); filter.setTimelineLimit(1);
filter.setIncludeLeaveRooms(true); filter.setIncludeLeaveRooms(true);
const localTimeoutMs = this.opts.pollTimeout + BUFFER_PERIOD_MS; const localTimeoutMs = this.opts.pollTimeout! + BUFFER_PERIOD_MS;
const filterId = await client.getOrCreateFilter(
getFilterName(client.credentials.userId!, "LEFT_ROOMS"), filter,
);
const qps: ISyncParams = { const qps: ISyncParams = {
timeout: 0, // don't want to block since this is a single isolated req timeout: 0, // don't want to block since this is a single isolated req
filter: filterId,
}; };
return client.getOrCreateFilter( const data = await client.http.authedRequest<ISyncResponse>(Method.Get, "/sync", qps as any, undefined, {
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter, localTimeoutMs,
).then(function(filterId) {
qps.filter = filterId;
return client.http.authedRequest<ISyncResponse>(Method.Get, "/sync", qps as any, undefined, {
localTimeoutMs,
});
}).then(async (data) => {
let leaveRooms = [];
if (data.rooms?.leave) {
leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
}
return Promise.all(leaveRooms.map(async (leaveObj) => {
const room = leaveObj.room;
if (!leaveObj.isBrandNewRoom) {
// the intention behind syncLeftRooms is to add in rooms which were
// *omitted* from the initial /sync. Rooms the user were joined to
// but then left whilst the app is running will appear in this list
// and we do not want to bother with them since they will have the
// current state already (and may get dupe messages if we add
// yet more timeline events!), so skip them.
// NB: When we persist rooms to localStorage this will be more
// complicated...
return;
}
leaveObj.timeline = leaveObj.timeline || {};
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
// set the back-pagination token. Do this *before* adding any
// events so that clients can start back-paginating.
room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS);
await this.processRoomEvents(room, stateEvents, events);
room.recalculate();
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
this.processEventsForNotifs(room, events);
return room;
}));
}); });
let leaveRooms: WrappedRoom<ILeftRoom>[] = [];
if (data.rooms?.leave) {
leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
}
const rooms = await Promise.all(leaveRooms.map(async (leaveObj) => {
const room = leaveObj.room;
if (!leaveObj.isBrandNewRoom) {
// the intention behind syncLeftRooms is to add in rooms which were
// *omitted* from the initial /sync. Rooms the user were joined to
// but then left whilst the app is running will appear in this list
// and we do not want to bother with them since they will have the
// current state already (and may get dupe messages if we add
// yet more timeline events!), so skip them.
// NB: When we persist rooms to localStorage this will be more
// complicated...
return;
}
leaveObj.timeline = leaveObj.timeline || {
prev_batch: null,
events: [],
};
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
// set the back-pagination token. Do this *before* adding any
// events so that clients can start back-paginating.
room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS);
await this.processRoomEvents(room, stateEvents, events);
room.recalculate();
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
this.processEventsForNotifs(room, events);
return room;
}));
return rooms.filter(Boolean) as Room[];
} }
/** /**
@ -348,7 +355,7 @@ export class SyncApi {
* store. * store.
*/ */
public peek(roomId: string): Promise<Room> { public peek(roomId: string): Promise<Room> {
if (this._peekRoom && this._peekRoom.roomId === roomId) { if (this._peekRoom?.roomId === roomId) {
return Promise.resolve(this._peekRoom); return Promise.resolve(this._peekRoom);
} }
@ -388,28 +395,28 @@ export class SyncApi {
// fire off pagination requests in response to the Room.timeline // fire off pagination requests in response to the Room.timeline
// events. // events.
if (response.messages.start) { if (response.messages.start) {
this._peekRoom.oldState.paginationToken = response.messages.start; this._peekRoom!.oldState.paginationToken = response.messages.start;
} }
// set the state of the room to as it was after the timeline executes // set the state of the room to as it was after the timeline executes
this._peekRoom.oldState.setStateEvents(oldStateEvents); this._peekRoom!.oldState.setStateEvents(oldStateEvents);
this._peekRoom.currentState.setStateEvents(stateEvents); this._peekRoom!.currentState.setStateEvents(stateEvents);
this.resolveInvites(this._peekRoom); this.resolveInvites(this._peekRoom!);
this._peekRoom.recalculate(); this._peekRoom!.recalculate();
// roll backwards to diverge old state. addEventsToTimeline // roll backwards to diverge old state. addEventsToTimeline
// will overwrite the pagination token, so make sure it overwrites // will overwrite the pagination token, so make sure it overwrites
// it with the right thing. // it with the right thing.
this._peekRoom.addEventsToTimeline(messages.reverse(), true, this._peekRoom!.addEventsToTimeline(messages.reverse(), true,
this._peekRoom.getLiveTimeline(), this._peekRoom!.getLiveTimeline(),
response.messages.start); response.messages.start);
client.store.storeRoom(this._peekRoom); client.store.storeRoom(this._peekRoom!);
client.emit(ClientEvent.Room, this._peekRoom); client.emit(ClientEvent.Room, this._peekRoom!);
this.peekPoll(this._peekRoom); this.peekPoll(this._peekRoom!);
return this._peekRoom; return this._peekRoom!;
}); });
} }
@ -437,7 +444,10 @@ export class SyncApi {
room_id: peekRoom.roomId, room_id: peekRoom.roomId,
timeout: String(30 * 1000), timeout: String(30 * 1000),
from: token, from: token,
}, undefined, { localTimeoutMs: 50 * 1000 }).then((res) => { }, undefined, {
localTimeoutMs: 50 * 1000,
abortSignal: this.abortController?.signal,
}).then((res) => {
if (this._peekRoom !== peekRoom) { if (this._peekRoom !== peekRoom) {
debuglog("Stopped peeking in room %s", peekRoom.roomId); debuglog("Stopped peeking in room %s", peekRoom.roomId);
return; return;
@ -487,7 +497,7 @@ export class SyncApi {
* @see module:client~MatrixClient#event:"sync" * @see module:client~MatrixClient#event:"sync"
* @return {?String} * @return {?String}
*/ */
public getSyncState(): SyncState { public getSyncState(): SyncState | null {
return this.syncState; return this.syncState;
} }
@ -499,18 +509,18 @@ export class SyncApi {
* this object. * this object.
* @return {?Object} * @return {?Object}
*/ */
public getSyncStateData(): ISyncStateData { public getSyncStateData(): ISyncStateData | null {
return this.syncStateData; return this.syncStateData ?? null;
} }
public async recoverFromSyncStartupError(savedSyncPromise: Promise<void>, err: MatrixError): Promise<void> { public async recoverFromSyncStartupError(savedSyncPromise: Promise<void> | undefined, error: Error): Promise<void> {
// Wait for the saved sync to complete - we send the pushrules and filter requests // Wait for the saved sync to complete - we send the pushrules and filter requests
// before the saved sync has finished so they can run in parallel, but only process // before the saved sync has finished so they can run in parallel, but only process
// the results after the saved sync is done. Equivalently, we wait for it to finish // the results after the saved sync is done. Equivalently, we wait for it to finish
// before reporting failures from these functions. // before reporting failures from these functions.
await savedSyncPromise; await savedSyncPromise;
const keepaliveProm = this.startKeepAlives(); const keepaliveProm = this.startKeepAlives();
this.updateSyncState(SyncState.Error, { error: err }); this.updateSyncState(SyncState.Error, { error });
await keepaliveProm; await keepaliveProm;
} }
@ -553,11 +563,11 @@ export class SyncApi {
this.client.pushRules = result; this.client.pushRules = result;
} catch (err) { } catch (err) {
logger.error("Getting push rules failed", err); logger.error("Getting push rules failed", err);
if (this.shouldAbortSync(err)) return; if (this.shouldAbortSync(<MatrixError>err)) return;
// wait for saved sync to complete before doing anything else, // wait for saved sync to complete before doing anything else,
// otherwise the sync state will end up being incorrect // otherwise the sync state will end up being incorrect
debuglog("Waiting for saved sync before retrying push rules..."); debuglog("Waiting for saved sync before retrying push rules...");
await this.recoverFromSyncStartupError(this.savedSyncPromise, err); await this.recoverFromSyncStartupError(this.savedSyncPromise, <Error>err);
return this.getPushRules(); // try again return this.getPushRules(); // try again
} }
}; };
@ -595,8 +605,7 @@ export class SyncApi {
const shouldClear = await this.wasLazyLoadingToggled(this.opts.lazyLoadMembers); const shouldClear = await this.wasLazyLoadingToggled(this.opts.lazyLoadMembers);
if (shouldClear) { if (shouldClear) {
this.storeIsInvalid = true; this.storeIsInvalid = true;
const reason = InvalidStoreError.TOGGLED_LAZY_LOADING; const error = new InvalidStoreError(InvalidStoreState.ToggledLazyLoading, !!this.opts.lazyLoadMembers);
const error = new InvalidStoreError(reason, !!this.opts.lazyLoadMembers);
this.updateSyncState(SyncState.Error, { error }); this.updateSyncState(SyncState.Error, { error });
// bail out of the sync loop now: the app needs to respond to this error. // bail out of the sync loop now: the app needs to respond to this error.
// we leave the state as 'ERROR' which isn't great since this normally means // we leave the state as 'ERROR' which isn't great since this normally means
@ -632,20 +641,20 @@ export class SyncApi {
let filterId: string; let filterId: string;
try { try {
filterId = await this.client.getOrCreateFilter(getFilterName(this.client.credentials.userId), filter); filterId = await this.client.getOrCreateFilter(getFilterName(this.client.credentials.userId!), filter);
} catch (err) { } catch (err) {
logger.error("Getting filter failed", err); logger.error("Getting filter failed", err);
if (this.shouldAbortSync(err)) return {}; if (this.shouldAbortSync(<MatrixError>err)) return {};
// wait for saved sync to complete before doing anything else, // wait for saved sync to complete before doing anything else,
// otherwise the sync state will end up being incorrect // otherwise the sync state will end up being incorrect
debuglog("Waiting for saved sync before retrying filter..."); debuglog("Waiting for saved sync before retrying filter...");
await this.recoverFromSyncStartupError(this.savedSyncPromise, err); await this.recoverFromSyncStartupError(this.savedSyncPromise, <Error>err);
return this.getFilter(); // try again return this.getFilter(); // try again
} }
return { filter, filterId }; return { filter, filterId };
}; };
private savedSyncPromise: Promise<void>; private savedSyncPromise?: Promise<void>;
/** /**
* Main entry point * Main entry point
@ -701,7 +710,7 @@ export class SyncApi {
// /notifications API somehow. // /notifications API somehow.
this.client.resetNotifTimelineSet(); this.client.resetNotifTimelineSet();
if (this.currentSyncRequest === null) { if (!this.currentSyncRequest) {
let firstSyncFilter = filterId; let firstSyncFilter = filterId;
const savedSyncToken = await savedSyncTokenPromise; const savedSyncToken = await savedSyncTokenPromise;
@ -711,7 +720,7 @@ export class SyncApi {
debuglog("Sending initial sync request..."); debuglog("Sending initial sync request...");
const initialFilter = this.buildDefaultFilter(); const initialFilter = this.buildDefaultFilter();
initialFilter.setDefinition(filter.getDefinition()); initialFilter.setDefinition(filter.getDefinition());
initialFilter.setTimelineLimit(this.opts.initialSyncLimit); initialFilter.setTimelineLimit(this.opts.initialSyncLimit!);
// Use an inline filter, no point uploading it for a single usage // Use an inline filter, no point uploading it for a single usage
firstSyncFilter = JSON.stringify(initialFilter.getDefinition()); firstSyncFilter = JSON.stringify(initialFilter.getDefinition());
} }
@ -742,7 +751,7 @@ export class SyncApi {
this.abortController?.abort(); this.abortController?.abort();
if (this.keepAliveTimer) { if (this.keepAliveTimer) {
clearTimeout(this.keepAliveTimer); clearTimeout(this.keepAliveTimer);
this.keepAliveTimer = null; this.keepAliveTimer = undefined;
} }
} }
@ -813,16 +822,16 @@ export class SyncApi {
let data: ISyncResponse; let data: ISyncResponse;
try { try {
//debuglog('Starting sync since=' + syncToken); //debuglog('Starting sync since=' + syncToken);
if (this.currentSyncRequest === null) { if (!this.currentSyncRequest) {
this.currentSyncRequest = this.doSyncRequest(syncOptions, syncToken); this.currentSyncRequest = this.doSyncRequest(syncOptions, syncToken);
} }
data = await this.currentSyncRequest; data = await this.currentSyncRequest;
} catch (e) { } catch (e) {
const abort = await this.onSyncError(e); const abort = await this.onSyncError(<MatrixError>e);
if (abort) return; if (abort) return;
continue; continue;
} finally { } finally {
this.currentSyncRequest = null; this.currentSyncRequest = undefined;
} }
//debuglog('Completed sync, next_batch=' + data.next_batch); //debuglog('Completed sync, next_batch=' + data.next_batch);
@ -838,7 +847,7 @@ export class SyncApi {
await this.client.store.setSyncData(data); await this.client.store.setSyncData(data);
const syncEventData = { const syncEventData = {
oldSyncToken: syncToken, oldSyncToken: syncToken ?? undefined,
nextSyncToken: data.next_batch, nextSyncToken: data.next_batch,
catchingUp: this.catchingUp, catchingUp: this.catchingUp,
}; };
@ -857,7 +866,7 @@ export class SyncApi {
logger.error("Caught /sync error", e); logger.error("Caught /sync error", e);
// Emit the exception for client handling // Emit the exception for client handling
this.client.emit(ClientEvent.SyncUnexpectedError, e); this.client.emit(ClientEvent.SyncUnexpectedError, <Error>e);
} }
// update this as it may have changed // update this as it may have changed
@ -897,13 +906,13 @@ export class SyncApi {
debuglog("Sync no longer running: exiting."); debuglog("Sync no longer running: exiting.");
if (this.connectionReturnedDefer) { if (this.connectionReturnedDefer) {
this.connectionReturnedDefer.reject(); this.connectionReturnedDefer.reject();
this.connectionReturnedDefer = null; this.connectionReturnedDefer = undefined;
} }
this.updateSyncState(SyncState.Stopped); this.updateSyncState(SyncState.Stopped);
} }
} }
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): Promise<ISyncResponse> { private doSyncRequest(syncOptions: ISyncOptions, syncToken: string | null): Promise<ISyncResponse> {
const qps = this.getSyncParams(syncOptions, syncToken); const qps = this.getSyncParams(syncOptions, syncToken);
return this.client.http.authedRequest<ISyncResponse>(Method.Get, "/sync", qps as any, undefined, { return this.client.http.authedRequest<ISyncResponse>(Method.Get, "/sync", qps as any, undefined, {
localTimeoutMs: qps.timeout + BUFFER_PERIOD_MS, localTimeoutMs: qps.timeout + BUFFER_PERIOD_MS,
@ -911,8 +920,8 @@ export class SyncApi {
}); });
} }
private getSyncParams(syncOptions: ISyncOptions, syncToken: string): ISyncParams { private getSyncParams(syncOptions: ISyncOptions, syncToken: string | null): ISyncParams {
let pollTimeout = this.opts.pollTimeout; let timeout = this.opts.pollTimeout!;
if (this.getSyncState() !== SyncState.Syncing || this.catchingUp) { if (this.getSyncState() !== SyncState.Syncing || this.catchingUp) {
// unless we are happily syncing already, we want the server to return // unless we are happily syncing already, we want the server to return
@ -927,7 +936,7 @@ export class SyncApi {
// for us. We do that by calling it with a zero timeout until it // for us. We do that by calling it with a zero timeout until it
// doesn't give us any more to_device messages. // doesn't give us any more to_device messages.
this.catchingUp = true; this.catchingUp = true;
pollTimeout = 0; timeout = 0;
} }
let filter = syncOptions.filter; let filter = syncOptions.filter;
@ -935,10 +944,7 @@ export class SyncApi {
filter = this.getGuestFilter(); filter = this.getGuestFilter();
} }
const qps: ISyncParams = { const qps: ISyncParams = { filter, timeout };
filter,
timeout: pollTimeout,
};
if (this.opts.disablePresence) { if (this.opts.disablePresence) {
qps.set_presence = SetPresence.Offline; qps.set_presence = SetPresence.Offline;
@ -953,7 +959,7 @@ export class SyncApi {
qps._cacheBuster = Date.now(); qps._cacheBuster = Date.now();
} }
if ([SyncState.Reconnecting, SyncState.Error].includes(this.getSyncState())) { if ([SyncState.Reconnecting, SyncState.Error].includes(this.getSyncState()!)) {
// we think the connection is dead. If it comes back up, we won't know // we think the connection is dead. If it comes back up, we won't know
// about it till /sync returns. If the timeout= is high, this could // about it till /sync returns. If the timeout= is high, this could
// be a long time. Set it to 0 when doing retries so we don't have to wait // be a long time. Set it to 0 when doing retries so we don't have to wait
@ -969,7 +975,7 @@ export class SyncApi {
debuglog("Sync no longer running: exiting"); debuglog("Sync no longer running: exiting");
if (this.connectionReturnedDefer) { if (this.connectionReturnedDefer) {
this.connectionReturnedDefer.reject(); this.connectionReturnedDefer.reject();
this.connectionReturnedDefer = null; this.connectionReturnedDefer = undefined;
} }
this.updateSyncState(SyncState.Stopped); this.updateSyncState(SyncState.Stopped);
return true; // abort return true; // abort
@ -994,7 +1000,7 @@ export class SyncApi {
// if they wish. // if they wish.
const keepAlivePromise = this.startKeepAlives(); const keepAlivePromise = this.startKeepAlives();
this.currentSyncRequest = null; this.currentSyncRequest = undefined;
// Transition from RECONNECTING to ERROR after a given number of failed syncs // Transition from RECONNECTING to ERROR after a given number of failed syncs
this.updateSyncState( this.updateSyncState(
this.failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ? this.failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ?
@ -1073,7 +1079,7 @@ export class SyncApi {
// handle presence events (User objects) // handle presence events (User objects)
if (Array.isArray(data.presence?.events)) { if (Array.isArray(data.presence?.events)) {
data.presence.events.map(client.getEventMapper()).forEach( data.presence!.events.map(client.getEventMapper()).forEach(
function(presenceEvent) { function(presenceEvent) {
let user = client.store.getUser(presenceEvent.getSender()); let user = client.store.getUser(presenceEvent.getSender());
if (user) { if (user) {
@ -1113,9 +1119,9 @@ export class SyncApi {
} }
// handle to-device events // handle to-device events
if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) { if (Array.isArray(data.to_device?.events) && data.to_device!.events.length > 0) {
const cancelledKeyVerificationTxns = []; const cancelledKeyVerificationTxns: string[] = [];
data.to_device.events data.to_device!.events
.filter((eventJSON) => { .filter((eventJSON) => {
if ( if (
eventJSON.type === EventType.RoomMessageEncrypted && eventJSON.type === EventType.RoomMessageEncrypted &&
@ -1137,7 +1143,7 @@ export class SyncApi {
// so we can flag the verification events as cancelled in the loop // so we can flag the verification events as cancelled in the loop
// below. // below.
if (toDeviceEvent.getType() === "m.key.verification.cancel") { if (toDeviceEvent.getType() === "m.key.verification.cancel") {
const txnId = toDeviceEvent.getContent()['transaction_id']; const txnId: string = toDeviceEvent.getContent()['transaction_id'];
if (txnId) { if (txnId) {
cancelledKeyVerificationTxns.push(txnId); cancelledKeyVerificationTxns.push(txnId);
} }
@ -1206,13 +1212,13 @@ export class SyncApi {
await this.processRoomEvents(room, stateEvents); await this.processRoomEvents(room, stateEvents);
const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId())?.getSender(); const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId()!)?.getSender();
if (client.isCryptoEnabled()) { if (client.isCryptoEnabled()) {
const parkedHistory = await client.crypto.cryptoStore.takeParkedSharedHistory(room.roomId); const parkedHistory = await client.crypto!.cryptoStore.takeParkedSharedHistory(room.roomId);
for (const parked of parkedHistory) { for (const parked of parkedHistory) {
if (parked.senderId === inviter) { if (parked.senderId === inviter) {
await client.crypto.olmDevice.addInboundGroupSession( await client.crypto!.olmDevice.addInboundGroupSession(
room.roomId, room.roomId,
parked.senderKey, parked.senderKey,
parked.forwardingCurve25519KeyChain, parked.forwardingCurve25519KeyChain,
@ -1256,7 +1262,7 @@ export class SyncApi {
if (joinObj.unread_notifications) { if (joinObj.unread_notifications) {
room.setUnreadNotificationCount( room.setUnreadNotificationCount(
NotificationCountType.Total, NotificationCountType.Total,
joinObj.unread_notifications.notification_count, joinObj.unread_notifications.notification_count ?? 0,
); );
// We track unread notifications ourselves in encrypted rooms, so don't // We track unread notifications ourselves in encrypted rooms, so don't
@ -1266,13 +1272,13 @@ export class SyncApi {
if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) { if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) {
room.setUnreadNotificationCount( room.setUnreadNotificationCount(
NotificationCountType.Highlight, NotificationCountType.Highlight,
joinObj.unread_notifications.highlight_count, joinObj.unread_notifications.highlight_count ?? 0,
); );
} }
} }
const unreadThreadNotifications = joinObj[UNREAD_THREAD_NOTIFICATIONS.name] const unreadThreadNotifications = joinObj[UNREAD_THREAD_NOTIFICATIONS.name]
?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName]; ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName!];
if (unreadThreadNotifications) { if (unreadThreadNotifications) {
// Only partially reset unread notification // Only partially reset unread notification
// We want to keep the client-generated count. Particularly important // We want to keep the client-generated count. Particularly important
@ -1283,7 +1289,7 @@ export class SyncApi {
room.setThreadUnreadNotificationCount( room.setThreadUnreadNotificationCount(
threadId, threadId,
NotificationCountType.Total, NotificationCountType.Total,
unreadNotification.notification_count, unreadNotification.notification_count ?? 0,
); );
const hasNoNotifications = const hasNoNotifications =
@ -1292,7 +1298,7 @@ export class SyncApi {
room.setThreadUnreadNotificationCount( room.setThreadUnreadNotificationCount(
threadId, threadId,
NotificationCountType.Highlight, NotificationCountType.Highlight,
unreadNotification.highlight_count, unreadNotification.highlight_count ?? 0,
); );
} }
} }
@ -1349,8 +1355,8 @@ export class SyncApi {
if (limited) { if (limited) {
room.resetLiveTimeline( room.resetLiveTimeline(
joinObj.timeline.prev_batch, joinObj.timeline.prev_batch,
this.opts.canResetEntireTimeline(room.roomId) ? this.opts.canResetEntireTimeline!(room.roomId) ?
null : syncEventData.oldSyncToken, null : (syncEventData.oldSyncToken ?? null),
); );
// We have to assume any gap in any timeline is // We have to assume any gap in any timeline is
@ -1448,7 +1454,7 @@ export class SyncApi {
return a.getTs() - b.getTs(); return a.getTs() - b.getTs();
}); });
this.notifEvents.forEach(function(event) { this.notifEvents.forEach(function(event) {
client.getNotifTimelineSet().addLiveEvent(event); client.getNotifTimelineSet()?.addLiveEvent(event);
}); });
} }
@ -1523,7 +1529,7 @@ export class SyncApi {
clearTimeout(this.keepAliveTimer); clearTimeout(this.keepAliveTimer);
if (this.connectionReturnedDefer) { if (this.connectionReturnedDefer) {
this.connectionReturnedDefer.resolve(connDidFail); this.connectionReturnedDefer.resolve(connDidFail);
this.connectionReturnedDefer = null; this.connectionReturnedDefer = undefined;
} }
}; };
@ -1639,7 +1645,7 @@ export class SyncApi {
// the code paths remain the same between invite/join display name stuff // the code paths remain the same between invite/join display name stuff
// which is a worthy trade-off for some minor pollution. // which is a worthy trade-off for some minor pollution.
const inviteEvent = member.events.member; const inviteEvent = member.events.member;
if (inviteEvent.getContent().membership !== "invite") { if (inviteEvent?.getContent().membership !== "invite") {
// between resolving and now they have since joined, so don't clobber // between resolving and now they have since joined, so don't clobber
return; return;
} }
@ -1802,7 +1808,7 @@ function createNewUser(client: MatrixClient, userId: string): User {
export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts: Partial<IStoredClientOpts>): Room { export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts: Partial<IStoredClientOpts>): Room {
const { timelineSupport } = client; const { timelineSupport } = client;
const room = new Room(roomId, client, client.getUserId(), { const room = new Room(roomId, client, client.getUserId()!, {
lazyLoadMembers: opts.lazyLoadMembers, lazyLoadMembers: opts.lazyLoadMembers,
pendingEventOrdering: opts.pendingEventOrdering, pendingEventOrdering: opts.pendingEventOrdering,
timelineSupport, timelineSupport,
@ -1832,7 +1838,7 @@ export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts:
// We need to add a listener for RoomState.members in order to hook them // We need to add a listener for RoomState.members in order to hook them
// correctly. // correctly.
room.on(RoomStateEvent.NewMember, (event, state, member) => { room.on(RoomStateEvent.NewMember, (event, state, member) => {
member.user = client.getUser(member.userId); member.user = client.getUser(member.userId) ?? undefined;
client.reEmitter.reEmit(member, [ client.reEmitter.reEmit(member, [
RoomMemberEvent.Name, RoomMemberEvent.Name,
RoomMemberEvent.Typing, RoomMemberEvent.Typing,

View File

@ -16,6 +16,8 @@ limitations under the License.
/** @module timeline-window */ /** @module timeline-window */
import { Optional } from "matrix-events-sdk";
import { Direction, EventTimeline } from './models/event-timeline'; import { Direction, EventTimeline } from './models/event-timeline';
import { logger } from './logger'; import { logger } from './logger';
import { MatrixClient } from "./client"; import { MatrixClient } from "./client";
@ -49,8 +51,8 @@ export class TimelineWindow {
// 'end' of the window. // 'end' of the window.
// //
// start.index is inclusive; end.index is exclusive. // start.index is inclusive; end.index is exclusive.
private start?: TimelineIndex = null; private start?: TimelineIndex;
private end?: TimelineIndex = null; private end?: TimelineIndex;
private eventCount = 0; private eventCount = 0;
/** /**
@ -102,7 +104,11 @@ export class TimelineWindow {
public load(initialEventId?: string, initialWindowSize = 20): Promise<void> { public load(initialEventId?: string, initialWindowSize = 20): Promise<void> {
// given an EventTimeline, find the event we were looking for, and initialise our // given an EventTimeline, find the event we were looking for, and initialise our
// fields so that the event in question is in the middle of the window. // fields so that the event in question is in the middle of the window.
const initFields = (timeline: EventTimeline) => { const initFields = (timeline: Optional<EventTimeline>) => {
if (!timeline) {
throw new Error("No timeline given to initFields");
}
let eventIndex: number; let eventIndex: number;
const events = timeline.getEvents(); const events = timeline.getEvents();
@ -153,11 +159,11 @@ export class TimelineWindow {
* @return {TimelineIndex} The requested timeline index if one exists, null * @return {TimelineIndex} The requested timeline index if one exists, null
* otherwise. * otherwise.
*/ */
public getTimelineIndex(direction: Direction): TimelineIndex { public getTimelineIndex(direction: Direction): TimelineIndex | null {
if (direction == EventTimeline.BACKWARDS) { if (direction == EventTimeline.BACKWARDS) {
return this.start; return this.start ?? null;
} else if (direction == EventTimeline.FORWARDS) { } else if (direction == EventTimeline.FORWARDS) {
return this.end; return this.end ?? null;
} else { } else {
throw new Error("Invalid direction '" + direction + "'"); throw new Error("Invalid direction '" + direction + "'");
} }
@ -299,7 +305,7 @@ export class TimelineWindow {
backwards: direction == EventTimeline.BACKWARDS, backwards: direction == EventTimeline.BACKWARDS,
limit: size, limit: size,
}).finally(function() { }).finally(function() {
tl.pendingPaginate = null; tl.pendingPaginate = undefined;
}).then((r) => { }).then((r) => {
debuglog("TimelineWindow: request completed with result " + r); debuglog("TimelineWindow: request completed with result " + r);
if (!r) { if (!r) {
@ -334,11 +340,17 @@ export class TimelineWindow {
*/ */
public unpaginate(delta: number, startOfTimeline: boolean): void { public unpaginate(delta: number, startOfTimeline: boolean): void {
const tl = startOfTimeline ? this.start : this.end; const tl = startOfTimeline ? this.start : this.end;
if (!tl) {
throw new Error(
`Attempting to unpaginate startOfTimeline=${startOfTimeline} but don't have this direction`,
);
}
// sanity-check the delta // sanity-check the delta
if (delta > this.eventCount || delta < 0) { if (delta > this.eventCount || delta < 0) {
throw new Error("Attemting to unpaginate " + delta + " events, but " + throw new Error(
"only have " + this.eventCount + " in the timeline"); `Attemting to unpaginate ${delta} events, but only have ${this.eventCount} in the timeline`,
);
} }
while (delta > 0) { while (delta > 0) {
@ -368,7 +380,7 @@ export class TimelineWindow {
return []; return [];
} }
const result = []; const result: MatrixEvent[] = [];
// iterate through each timeline between this.start and this.end // iterate through each timeline between this.start and this.end
// (inclusive). // (inclusive).
@ -390,7 +402,7 @@ export class TimelineWindow {
if (timeline === this.start.timeline) { if (timeline === this.start.timeline) {
startIndex = this.start.index + timeline.getBaseIndex(); startIndex = this.start.index + timeline.getBaseIndex();
} }
if (timeline === this.end.timeline) { if (timeline === this.end?.timeline) {
endIndex = this.end.index + timeline.getBaseIndex(); endIndex = this.end.index + timeline.getBaseIndex();
} }
@ -399,10 +411,10 @@ export class TimelineWindow {
} }
// if we're not done, iterate to the next timeline. // if we're not done, iterate to the next timeline.
if (timeline === this.end.timeline) { if (timeline === this.end?.timeline) {
break; break;
} else { } else {
timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS); timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)!;
} }
} }

View File

@ -48,7 +48,7 @@ export function internaliseString(str: string): string {
} }
// Return any cached string reference // Return any cached string reference
return interns.get(str); return interns.get(str)!;
} }
/** /**
@ -412,7 +412,7 @@ export function defer<T = void>(): IDeferred<T> {
export async function promiseMapSeries<T>( export async function promiseMapSeries<T>(
promises: Array<T | Promise<T>>, promises: Array<T | Promise<T>>,
fn: (t: T) => Promise<unknown> | void, // if async/promise we don't care about the type as we only await resolution fn: (t: T) => Promise<unknown> | undefined, // if async we don't care about the type as we only await resolution
): Promise<void> { ): Promise<void> {
for (const o of promises) { for (const o of promises) {
await fn(await o); await fn(await o);

View File

@ -47,6 +47,7 @@ import { CallFeed } from './callFeed';
import { MatrixClient } from "../client"; import { MatrixClient } from "../client";
import { ISendEventResponse } from "../@types/requests"; import { ISendEventResponse } from "../@types/requests";
import { EventEmitterEvents, TypedEventEmitter } from "../models/typed-event-emitter"; import { EventEmitterEvents, TypedEventEmitter } from "../models/typed-event-emitter";
import { MatrixError } from "../http-api";
// events: hangup, error(err), replaced(call), state(state, oldState) // events: hangup, error(err), replaced(call), state(state, oldState)
@ -68,7 +69,7 @@ import { EventEmitterEvents, TypedEventEmitter } from "../models/typed-event-emi
interface CallOpts { interface CallOpts {
roomId?: string; roomId?: string;
client?: any; // Fix when client is TSified client: MatrixClient;
forceTURN?: boolean; forceTURN?: boolean;
turnServers?: Array<TurnServer>; turnServers?: Array<TurnServer>;
} }
@ -227,7 +228,7 @@ const FALLBACK_ICE_SERVER = 'stun:turn.matrix.org';
const CALL_TIMEOUT_MS = 60000; const CALL_TIMEOUT_MS = 60000;
export class CallError extends Error { export class CallError extends Error {
code: string; public readonly code: string;
constructor(code: CallErrorCode, msg: string, err: Error) { constructor(code: CallErrorCode, msg: string, err: Error) {
// Still don't think there's any way to have proper nested errors // Still don't think there's any way to have proper nested errors
@ -271,33 +272,33 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
public roomId: string; public roomId: string;
public callId: string; public callId: string;
public state = CallState.Fledgling; public state = CallState.Fledgling;
public hangupParty: CallParty; public hangupParty?: CallParty;
public hangupReason: string; public hangupReason?: string;
public direction: CallDirection; public direction?: CallDirection;
public ourPartyId: string; public ourPartyId: string;
private client: MatrixClient; private readonly client: MatrixClient;
private forceTURN: boolean; private readonly forceTURN: boolean;
private turnServers: Array<TurnServer>; private readonly turnServers: Array<TurnServer>;
// A queue for candidates waiting to go out. // A queue for candidates waiting to go out.
// We try to amalgamate candidates into a single candidate message where // We try to amalgamate candidates into a single candidate message where
// possible // possible
private candidateSendQueue: Array<RTCIceCandidate> = []; private candidateSendQueue: Array<RTCIceCandidate> = [];
private candidateSendTries = 0; private candidateSendTries = 0;
private sentEndOfCandidates = false; private sentEndOfCandidates = false;
private peerConn: RTCPeerConnection; private peerConn?: RTCPeerConnection;
private feeds: Array<CallFeed> = []; private feeds: Array<CallFeed> = [];
private usermediaSenders: Array<RTCRtpSender> = []; private usermediaSenders: Array<RTCRtpSender> = [];
private screensharingSenders: Array<RTCRtpSender> = []; private screensharingSenders: Array<RTCRtpSender> = [];
private inviteOrAnswerSent = false; private inviteOrAnswerSent = false;
private waitForLocalAVStream: boolean; private waitForLocalAVStream: boolean;
private successor: MatrixCall; private successor?: MatrixCall;
private opponentMember: RoomMember; private opponentMember?: RoomMember;
private opponentVersion: number | string; private opponentVersion?: number | string;
// The party ID of the other side: undefined if we haven't chosen a partner // The party ID of the other side: undefined if we haven't chosen a partner
// yet, null if we have but they didn't send a party ID. // yet, null if we have but they didn't send a party ID.
private opponentPartyId: string; private opponentPartyId?: string | null;
private opponentCaps: CallCapabilities; private opponentCaps?: CallCapabilities;
private inviteTimeout?: ReturnType<typeof setTimeout>; private inviteTimeout?: ReturnType<typeof setTimeout>;
// The logic of when & if a call is on hold is nontrivial and explained in is*OnHold // The logic of when & if a call is on hold is nontrivial and explained in is*OnHold
@ -307,7 +308,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// the stats for the call at the point it ended. We can't get these after we // the stats for the call at the point it ended. We can't get these after we
// tear the call down, so we just grab a snapshot before we stop the call. // tear the call down, so we just grab a snapshot before we stop the call.
// The typescript definitions have this type as 'any' :( // The typescript definitions have this type as 'any' :(
private callStatsAtEnd: any[]; private callStatsAtEnd?: any[];
// Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example // Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
private makingOffer = false; private makingOffer = false;
@ -318,9 +319,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// the call) we buffer them up here so we can then add the ones from the party we pick // the call) we buffer them up here so we can then add the ones from the party we pick
private remoteCandidateBuffer = new Map<string, RTCIceCandidate[]>(); private remoteCandidateBuffer = new Map<string, RTCIceCandidate[]>();
private remoteAssertedIdentity: AssertedIdentity; private remoteAssertedIdentity?: AssertedIdentity;
private remoteSDPStreamMetadata?: SDPStreamMetadata;
private remoteSDPStreamMetadata: SDPStreamMetadata;
private callLengthInterval?: ReturnType<typeof setInterval>; private callLengthInterval?: ReturnType<typeof setInterval>;
private callLength = 0; private callLength = 0;
@ -366,13 +366,13 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
* @param options An object providing configuration options for the data channel. * @param options An object providing configuration options for the data channel.
*/ */
public createDataChannel(label: string, options: RTCDataChannelInit) { public createDataChannel(label: string, options: RTCDataChannelInit) {
const dataChannel = this.peerConn.createDataChannel(label, options); const dataChannel = this.peerConn!.createDataChannel(label, options);
this.emit(CallEvent.DataChannel, dataChannel); this.emit(CallEvent.DataChannel, dataChannel);
logger.debug("created data channel"); logger.debug("created data channel");
return dataChannel; return dataChannel;
} }
public getOpponentMember(): RoomMember { public getOpponentMember(): RoomMember | undefined {
return this.opponentMember; return this.opponentMember;
} }
@ -384,7 +384,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
return Boolean(this.opponentCaps && this.opponentCaps["m.call.dtmf"]); return Boolean(this.opponentCaps && this.opponentCaps["m.call.dtmf"]);
} }
public getRemoteAssertedIdentity(): AssertedIdentity { public getRemoteAssertedIdentity(): AssertedIdentity | undefined {
return this.remoteAssertedIdentity; return this.remoteAssertedIdentity;
} }
@ -395,64 +395,58 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
public get hasLocalUserMediaVideoTrack(): boolean { public get hasLocalUserMediaVideoTrack(): boolean {
return this.localUsermediaStream?.getVideoTracks().length > 0; return !!this.localUsermediaStream?.getVideoTracks().length;
} }
public get hasRemoteUserMediaVideoTrack(): boolean { public get hasRemoteUserMediaVideoTrack(): boolean {
return this.getRemoteFeeds().some((feed) => { return this.getRemoteFeeds().some((feed) => {
return ( return feed.purpose === SDPStreamMetadataPurpose.Usermedia && feed.stream?.getVideoTracks().length;
feed.purpose === SDPStreamMetadataPurpose.Usermedia &&
feed.stream.getVideoTracks().length > 0
);
}); });
} }
public get hasLocalUserMediaAudioTrack(): boolean { public get hasLocalUserMediaAudioTrack(): boolean {
return this.localUsermediaStream?.getAudioTracks().length > 0; return !!this.localUsermediaStream?.getAudioTracks().length;
} }
public get hasRemoteUserMediaAudioTrack(): boolean { public get hasRemoteUserMediaAudioTrack(): boolean {
return this.getRemoteFeeds().some((feed) => { return this.getRemoteFeeds().some((feed) => {
return ( return feed.purpose === SDPStreamMetadataPurpose.Usermedia && !!feed.stream?.getAudioTracks().length;
feed.purpose === SDPStreamMetadataPurpose.Usermedia &&
feed.stream.getAudioTracks().length > 0
);
}); });
} }
public get localUsermediaFeed(): CallFeed { public get localUsermediaFeed(): CallFeed | undefined {
return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia); return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia);
} }
public get localScreensharingFeed(): CallFeed { public get localScreensharingFeed(): CallFeed | undefined {
return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare); return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare);
} }
public get localUsermediaStream(): MediaStream { public get localUsermediaStream(): MediaStream | undefined {
return this.localUsermediaFeed?.stream; return this.localUsermediaFeed?.stream;
} }
public get localScreensharingStream(): MediaStream { public get localScreensharingStream(): MediaStream | undefined {
return this.localScreensharingFeed?.stream; return this.localScreensharingFeed?.stream;
} }
public get remoteUsermediaFeed(): CallFeed { public get remoteUsermediaFeed(): CallFeed | undefined {
return this.getRemoteFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia); return this.getRemoteFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia);
} }
public get remoteScreensharingFeed(): CallFeed { public get remoteScreensharingFeed(): CallFeed | undefined {
return this.getRemoteFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare); return this.getRemoteFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare);
} }
public get remoteUsermediaStream(): MediaStream { public get remoteUsermediaStream(): MediaStream | undefined {
return this.remoteUsermediaFeed?.stream; return this.remoteUsermediaFeed?.stream;
} }
public get remoteScreensharingStream(): MediaStream { public get remoteScreensharingStream(): MediaStream | undefined {
return this.remoteScreensharingFeed?.stream; return this.remoteScreensharingFeed?.stream;
} }
private getFeedByStreamId(streamId: string): CallFeed { private getFeedByStreamId(streamId: string): CallFeed | undefined {
return this.getFeeds().find((feed) => feed.stream.id === streamId); return this.getFeeds().find((feed) => feed.stream.id === streamId);
} }
@ -513,10 +507,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
return; return;
} }
const userId = this.getOpponentMember().userId; const userId = this.getOpponentMember()!.userId;
const purpose = this.remoteSDPStreamMetadata[stream.id].purpose; const purpose = this.remoteSDPStreamMetadata![stream.id].purpose;
const audioMuted = this.remoteSDPStreamMetadata[stream.id].audio_muted; const audioMuted = this.remoteSDPStreamMetadata![stream.id].audio_muted;
const videoMuted = this.remoteSDPStreamMetadata[stream.id].video_muted; const videoMuted = this.remoteSDPStreamMetadata![stream.id].video_muted;
if (!purpose) { if (!purpose) {
logger.warn(`Ignoring stream with id ${stream.id} because we didn't get any metadata about it`); logger.warn(`Ignoring stream with id ${stream.id} because we didn't get any metadata about it`);
@ -546,7 +540,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
* This method is used ONLY if the other client doesn't support sending SDPStreamMetadata * This method is used ONLY if the other client doesn't support sending SDPStreamMetadata
*/ */
private pushRemoteFeedWithoutMetadata(stream: MediaStream): void { private pushRemoteFeedWithoutMetadata(stream: MediaStream): void {
const userId = this.getOpponentMember().userId; const userId = this.getOpponentMember()!.userId;
// We can guess the purpose here since the other client can only send one stream // We can guess the purpose here since the other client can only send one stream
const purpose = SDPStreamMetadataPurpose.Usermedia; const purpose = SDPStreamMetadataPurpose.Usermedia;
const oldRemoteStream = this.feeds.find((feed) => !feed.isLocal())?.stream; const oldRemoteStream = this.feeds.find((feed) => !feed.isLocal())?.stream;
@ -580,7 +574,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
private pushNewLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true): void { private pushNewLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true): void {
const userId = this.client.getUserId(); const userId = this.client.getUserId()!;
// Tracks don't always start off enabled, eg. chrome will give a disabled // Tracks don't always start off enabled, eg. chrome will give a disabled
// audio track if you ask for user media audio and already had one that // audio track if you ask for user media audio and already had one that
@ -631,7 +625,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
`enabled=${track.enabled}` + `enabled=${track.enabled}` +
`) to peer connection`, `) to peer connection`,
); );
senderArray.push(this.peerConn.addTrack(track, callFeed.stream)); senderArray.push(this.peerConn!.addTrack(track, callFeed.stream));
} }
} }
@ -656,7 +650,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
: this.screensharingSenders; : this.screensharingSenders;
for (const sender of senderArray) { for (const sender of senderArray) {
this.peerConn.removeTrack(sender); this.peerConn?.removeTrack(sender);
} }
// Empty the array // Empty the array
senderArray.splice(0, senderArray.length); senderArray.splice(0, senderArray.length);
@ -687,7 +681,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
// The typescript definitions have this type as 'any' :( // The typescript definitions have this type as 'any' :(
public async getCurrentCallStats(): Promise<any[]> { public async getCurrentCallStats(): Promise<any[] | undefined> {
if (this.callHasEnded()) { if (this.callHasEnded()) {
return this.callStatsAtEnd; return this.callStatsAtEnd;
} }
@ -695,7 +689,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
return this.collectCallStats(); return this.collectCallStats();
} }
private async collectCallStats(): Promise<any[]> { private async collectCallStats(): Promise<any[] | undefined> {
// This happens when the call fails before it starts. // This happens when the call fails before it starts.
// For example when we fail to get capture sources // For example when we fail to get capture sources
if (!this.peerConn) return; if (!this.peerConn) return;
@ -765,8 +759,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.hangupParty = CallParty.Remote; // effectively this.hangupParty = CallParty.Remote; // effectively
this.setState(CallState.Ended); this.setState(CallState.Ended);
this.stopAllMedia(); this.stopAllMedia();
if (this.peerConn.signalingState != 'closed') { if (this.peerConn?.signalingState != 'closed') {
this.peerConn.close(); this.peerConn?.close();
} }
this.emit(CallEvent.Hangup); this.emit(CallEvent.Hangup);
} }
@ -786,7 +780,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
private shouldAnswerWithMediaType( private shouldAnswerWithMediaType(
wantedValue: boolean | undefined, valueOfTheOtherSide: boolean | undefined, type: "audio" | "video", wantedValue: boolean | undefined,
valueOfTheOtherSide: boolean | undefined,
type: "audio" | "video",
): boolean { ): boolean {
if (wantedValue && !valueOfTheOtherSide) { if (wantedValue && !valueOfTheOtherSide) {
// TODO: Figure out how to do this // TODO: Figure out how to do this
@ -832,7 +828,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const usermediaFeed = new CallFeed({ const usermediaFeed = new CallFeed({
client: this.client, client: this.client,
roomId: this.roomId, roomId: this.roomId,
userId: this.client.getUserId(), userId: this.client.getUserId()!,
stream, stream,
purpose: SDPStreamMetadataPurpose.Usermedia, purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: false, audioMuted: false,
@ -854,7 +850,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.waitForLocalAVStream = false; this.waitForLocalAVStream = false;
await this.answer(answerWithAudio, false); await this.answer(answerWithAudio, false);
} else { } else {
this.getUserMediaFailed(e); this.getUserMediaFailed(<Error>e);
return; return;
} }
} }
@ -953,7 +949,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} catch (error) { } catch (error) {
logger.error("Failed to upgrade the call", error); logger.error("Failed to upgrade the call", error);
this.emit(CallEvent.Error, this.emit(CallEvent.Error,
new CallError(CallErrorCode.NoUserMedia, "Failed to get camera access: ", error), new CallError(CallErrorCode.NoUserMedia, "Failed to get camera access: ", <Error>error),
); );
} }
} }
@ -1008,10 +1004,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
} else { } else {
for (const sender of this.screensharingSenders) { for (const sender of this.screensharingSenders) {
this.peerConn.removeTrack(sender); this.peerConn?.removeTrack(sender);
} }
this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream); this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream!);
this.deleteFeedByStream(this.localScreensharingStream); this.deleteFeedByStream(this.localScreensharingStream!);
return false; return false;
} }
} }
@ -1032,13 +1028,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const stream = await this.client.getMediaHandler().getScreensharingStream(desktopCapturerSourceId); const stream = await this.client.getMediaHandler().getScreensharingStream(desktopCapturerSourceId);
if (!stream) return false; if (!stream) return false;
const track = stream.getTracks().find((track) => { const track = stream.getTracks().find(track => track.kind === "video");
return track.kind === "video"; const sender = this.usermediaSenders.find(sender => sender.track?.kind === "video");
}); sender?.replaceTrack(track ?? null);
const sender = this.usermediaSenders.find((sender) => {
return sender.track?.kind === "video";
});
sender.replaceTrack(track);
this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare, false); this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare, false);
@ -1048,16 +1040,12 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
return false; return false;
} }
} else { } else {
const track = this.localUsermediaStream.getTracks().find((track) => { const track = this.localUsermediaStream?.getTracks().find((track) => track.kind === "video");
return track.kind === "video"; const sender = this.usermediaSenders.find((sender) => sender.track?.kind === "video");
}); sender?.replaceTrack(track ?? null);
const sender = this.usermediaSenders.find((sender) => {
return sender.track?.kind === "video";
});
sender.replaceTrack(track);
this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream); this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream!);
this.deleteFeedByStream(this.localScreensharingStream); this.deleteFeedByStream(this.localScreensharingStream!);
return false; return false;
} }
@ -1070,22 +1058,22 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
public async updateLocalUsermediaStream( public async updateLocalUsermediaStream(
stream: MediaStream, forceAudio = false, forceVideo = false, stream: MediaStream, forceAudio = false, forceVideo = false,
): Promise<void> { ): Promise<void> {
const callFeed = this.localUsermediaFeed; const callFeed = this.localUsermediaFeed!;
const audioEnabled = forceAudio || (!callFeed.isAudioMuted() && !this.remoteOnHold); const audioEnabled = forceAudio || (!callFeed.isAudioMuted() && !this.remoteOnHold);
const videoEnabled = forceVideo || (!callFeed.isVideoMuted() && !this.remoteOnHold); const videoEnabled = forceVideo || (!callFeed.isVideoMuted() && !this.remoteOnHold);
setTracksEnabled(stream.getAudioTracks(), audioEnabled); setTracksEnabled(stream.getAudioTracks(), audioEnabled);
setTracksEnabled(stream.getVideoTracks(), videoEnabled); setTracksEnabled(stream.getVideoTracks(), videoEnabled);
// We want to keep the same stream id, so we replace the tracks rather than the whole stream // We want to keep the same stream id, so we replace the tracks rather than the whole stream
for (const track of this.localUsermediaStream.getTracks()) { for (const track of this.localUsermediaStream!.getTracks()) {
this.localUsermediaStream.removeTrack(track); this.localUsermediaStream!.removeTrack(track);
track.stop(); track.stop();
} }
for (const track of stream.getTracks()) { for (const track of stream.getTracks()) {
this.localUsermediaStream.addTrack(track); this.localUsermediaStream!.addTrack(track);
} }
const newSenders = []; const newSenders: RTCRtpSender[] = [];
for (const track of stream.getTracks()) { for (const track of stream.getTracks()) {
const oldSender = this.usermediaSenders.find((sender) => sender.track?.kind === track.kind); const oldSender = this.usermediaSenders.find((sender) => sender.track?.kind === track.kind);
@ -1111,7 +1099,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
`streamPurpose="${callFeed.purpose}"` + `streamPurpose="${callFeed.purpose}"` +
`) to peer connection`, `) to peer connection`,
); );
newSender = this.peerConn.addTrack(track, this.localUsermediaStream); newSender = this.peerConn!.addTrack(track, this.localUsermediaStream!);
} }
newSenders.push(newSender); newSenders.push(newSender);
@ -1149,7 +1137,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
* (including if the call is not set up yet). * (including if the call is not set up yet).
*/ */
public isLocalVideoMuted(): boolean { public isLocalVideoMuted(): boolean {
return this.localUsermediaFeed?.isVideoMuted(); return this.localUsermediaFeed?.isVideoMuted() ?? false;
} }
/** /**
@ -1181,7 +1169,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
* is not set up yet). * is not set up yet).
*/ */
public isMicrophoneMuted(): boolean { public isMicrophoneMuted(): boolean {
return this.localUsermediaFeed?.isAudioMuted(); return this.localUsermediaFeed?.isAudioMuted() ?? false;
} }
/** /**
@ -1196,7 +1184,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
if (this.isRemoteOnHold() === onHold) return; if (this.isRemoteOnHold() === onHold) return;
this.remoteOnHold = onHold; this.remoteOnHold = onHold;
for (const transceiver of this.peerConn.getTransceivers()) { for (const transceiver of this.peerConn!.getTransceivers()) {
// We don't send hold music or anything so we're not actually // We don't send hold music or anything so we're not actually
// sending anything, but sendrecv is fairly standard for hold and // sending anything, but sendrecv is fairly standard for hold and
// it makes it a lot easier to figure out who's put who on hold. // it makes it a lot easier to figure out who's put who on hold.
@ -1219,8 +1207,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// We consider a call to be on hold only if *all* the tracks are on hold // We consider a call to be on hold only if *all* the tracks are on hold
// (is this the right thing to do?) // (is this the right thing to do?)
for (const transceiver of this.peerConn.getTransceivers()) { for (const transceiver of this.peerConn!.getTransceivers()) {
const trackOnHold = ['inactive', 'recvonly'].includes(transceiver.currentDirection); const trackOnHold = ['inactive', 'recvonly'].includes(transceiver.currentDirection!);
if (!trackOnHold) callOnHold = false; if (!trackOnHold) callOnHold = false;
} }
@ -1233,8 +1221,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
* @param digit The digit (nb. string - '#' and '*' are dtmf too) * @param digit The digit (nb. string - '#' and '*' are dtmf too)
*/ */
public sendDtmfDigit(digit: string): void { public sendDtmfDigit(digit: string): void {
for (const sender of this.peerConn.getSenders()) { for (const sender of this.peerConn!.getSenders()) {
if (sender.track.kind === 'audio' && sender.dtmf) { if (sender.track?.kind === 'audio' && sender.dtmf) {
sender.dtmf.insertDTMF(digit); sender.dtmf.insertDTMF(digit);
return; return;
} }
@ -1251,8 +1239,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const micShouldBeMuted = this.isMicrophoneMuted() || this.remoteOnHold; const micShouldBeMuted = this.isMicrophoneMuted() || this.remoteOnHold;
const vidShouldBeMuted = this.isLocalVideoMuted() || this.remoteOnHold; const vidShouldBeMuted = this.isLocalVideoMuted() || this.remoteOnHold;
setTracksEnabled(this.localUsermediaStream.getAudioTracks(), !micShouldBeMuted); setTracksEnabled(this.localUsermediaStream!.getAudioTracks(), !micShouldBeMuted);
setTracksEnabled(this.localUsermediaStream.getVideoTracks(), !vidShouldBeMuted); setTracksEnabled(this.localUsermediaStream!.getVideoTracks(), !vidShouldBeMuted);
} }
private gotCallFeedsForInvite(callFeeds: CallFeed[]): void { private gotCallFeedsForInvite(callFeeds: CallFeed[]): void {
@ -1278,10 +1266,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
private async sendAnswer(): Promise<void> { private async sendAnswer(): Promise<void> {
const answerContent = { const answerContent = {
answer: { answer: {
sdp: this.peerConn.localDescription.sdp, sdp: this.peerConn!.localDescription!.sdp,
// type is now deprecated as of Matrix VoIP v1, but // type is now deprecated as of Matrix VoIP v1, but
// required to still be sent for backwards compat // required to still be sent for backwards compat
type: this.peerConn.localDescription.type, type: this.peerConn!.localDescription!.type,
}, },
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(), [SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(),
} as MCallAnswer; } as MCallAnswer;
@ -1305,15 +1293,15 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} catch (error) { } catch (error) {
// We've failed to answer: back to the ringing state // We've failed to answer: back to the ringing state
this.setState(CallState.Ringing); this.setState(CallState.Ringing);
this.client.cancelPendingEvent(error.event); if (error instanceof MatrixError && error.event) this.client.cancelPendingEvent(error.event);
let code = CallErrorCode.SendAnswer; let code = CallErrorCode.SendAnswer;
let message = "Failed to send answer"; let message = "Failed to send answer";
if (error.name == 'UnknownDeviceError') { if ((<Error>error).name == 'UnknownDeviceError') {
code = CallErrorCode.UnknownDevices; code = CallErrorCode.UnknownDevices;
message = "Unknown devices present in the room"; message = "Unknown devices present in the room";
} }
this.emit(CallEvent.Error, new CallError(code, message, error)); this.emit(CallEvent.Error, new CallError(code, message, <Error>error));
throw error; throw error;
} }
@ -1333,10 +1321,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.setState(CallState.CreateAnswer); this.setState(CallState.CreateAnswer);
let myAnswer; let myAnswer: RTCSessionDescriptionInit;
try { try {
this.getRidOfRTXCodecs(); this.getRidOfRTXCodecs();
myAnswer = await this.peerConn.createAnswer(); myAnswer = await this.peerConn!.createAnswer();
} catch (err) { } catch (err) {
logger.debug("Failed to create answer: ", err); logger.debug("Failed to create answer: ", err);
this.terminate(CallParty.Local, CallErrorCode.CreateAnswer, true); this.terminate(CallParty.Local, CallErrorCode.CreateAnswer, true);
@ -1344,7 +1332,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
try { try {
await this.peerConn.setLocalDescription(myAnswer); await this.peerConn!.setLocalDescription(myAnswer);
this.setState(CallState.Connecting); this.setState(CallState.Connecting);
// Allow a short time for initial candidates to be gathered // Allow a short time for initial candidates to be gathered
@ -1364,7 +1352,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
* Internal * Internal
* @param {Object} event * @param {Object} event
*/ */
private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent): Promise<void> => { private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent): void => {
if (event.candidate) { if (event.candidate) {
logger.debug( logger.debug(
"Call " + this.callId + " got local ICE " + event.candidate.sdpMid + " candidate: " + "Call " + this.callId + " got local ICE " + event.candidate.sdpMid + " candidate: " +
@ -1384,8 +1372,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
}; };
private onIceGatheringStateChange = (event: Event): void => { private onIceGatheringStateChange = (event: Event): void => {
logger.debug("ice gathering state changed to " + this.peerConn.iceGatheringState); logger.debug("ice gathering state changed to " + this.peerConn?.iceGatheringState);
if (this.peerConn.iceGatheringState === 'complete' && !this.sentEndOfCandidates) { if (this.peerConn?.iceGatheringState === 'complete' && !this.sentEndOfCandidates) {
// If we didn't get an empty-string candidate to signal the end of candidates, // If we didn't get an empty-string candidate to signal the end of candidates,
// create one ourselves now gathering has finished. // create one ourselves now gathering has finished.
// We cast because the interface lists all the properties as required but we // We cast because the interface lists all the properties as required but we
@ -1471,7 +1459,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
try { try {
await this.peerConn.setRemoteDescription(content.answer); await this.peerConn!.setRemoteDescription(content.answer);
} catch (e) { } catch (e) {
logger.debug("Failed to set remote description", e); logger.debug("Failed to set remote description", e);
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false); this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
@ -1530,7 +1518,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation // https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation
const offerCollision = ( const offerCollision = (
(description.type === 'offer') && (description.type === 'offer') &&
(this.makingOffer || this.peerConn.signalingState !== 'stable') (this.makingOffer || this.peerConn!.signalingState !== 'stable')
); );
this.ignoreOffer = !polite && offerCollision; this.ignoreOffer = !polite && offerCollision;
@ -1549,15 +1537,15 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
try { try {
await this.peerConn.setRemoteDescription(description); await this.peerConn!.setRemoteDescription(description);
if (description.type === 'offer') { if (description.type === 'offer') {
this.getRidOfRTXCodecs(); this.getRidOfRTXCodecs();
const localDescription = await this.peerConn.createAnswer(); const localDescription = await this.peerConn!.createAnswer();
await this.peerConn.setLocalDescription(localDescription); await this.peerConn!.setLocalDescription(localDescription);
this.sendVoipEvent(EventType.CallNegotiate, { this.sendVoipEvent(EventType.CallNegotiate, {
description: this.peerConn.localDescription, description: this.peerConn!.localDescription,
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(), [SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(),
}); });
} }
@ -1577,10 +1565,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.remoteSDPStreamMetadata = utils.recursivelyAssign(this.remoteSDPStreamMetadata || {}, metadata, true); this.remoteSDPStreamMetadata = utils.recursivelyAssign(this.remoteSDPStreamMetadata || {}, metadata, true);
for (const feed of this.getRemoteFeeds()) { for (const feed of this.getRemoteFeeds()) {
const streamId = feed.stream.id; const streamId = feed.stream.id;
const metadata = this.remoteSDPStreamMetadata[streamId]; const metadata = this.remoteSDPStreamMetadata![streamId];
feed.setAudioVideoMuted(metadata?.audio_muted, metadata?.video_muted); feed.setAudioVideoMuted(metadata?.audio_muted, metadata?.video_muted);
feed.purpose = this.remoteSDPStreamMetadata[streamId]?.purpose; feed.purpose = this.remoteSDPStreamMetadata![streamId]?.purpose;
} }
} }
@ -1618,14 +1606,14 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
try { try {
await this.peerConn.setLocalDescription(description); await this.peerConn!.setLocalDescription(description);
} catch (err) { } catch (err) {
logger.debug("Error setting local description!", err); logger.debug("Error setting local description!", err);
this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true); this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true);
return; return;
} }
if (this.peerConn.iceGatheringState === 'gathering') { if (this.peerConn!.iceGatheringState === 'gathering') {
// Allow a short time for initial candidates to be gathered // Allow a short time for initial candidates to be gathered
await new Promise(resolve => { await new Promise(resolve => {
setTimeout(resolve, 200); setTimeout(resolve, 200);
@ -1642,9 +1630,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// clunky because TypeScript can't follow the types through if we use an expression as the key // clunky because TypeScript can't follow the types through if we use an expression as the key
if (this.state === CallState.CreateOffer) { if (this.state === CallState.CreateOffer) {
content.offer = this.peerConn.localDescription; content.offer = this.peerConn!.localDescription!;
} else { } else {
content.description = this.peerConn.localDescription; content.description = this.peerConn!.localDescription!;
} }
content.capabilities = { content.capabilities = {
@ -1663,7 +1651,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
await this.sendVoipEvent(eventType, content); await this.sendVoipEvent(eventType, content);
} catch (error) { } catch (error) {
logger.error("Failed to send invite", error); logger.error("Failed to send invite", error);
if (error.event) this.client.cancelPendingEvent(error.event); if (error instanceof MatrixError && error.event) this.client.cancelPendingEvent(error.event);
let code = CallErrorCode.SignallingFailed; let code = CallErrorCode.SignallingFailed;
let message = "Signalling failed"; let message = "Signalling failed";
@ -1671,12 +1659,12 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
code = CallErrorCode.SendInvite; code = CallErrorCode.SendInvite;
message = "Failed to send invite"; message = "Failed to send invite";
} }
if (error.name == 'UnknownDeviceError') { if ((<Error>error).name == 'UnknownDeviceError') {
code = CallErrorCode.UnknownDevices; code = CallErrorCode.UnknownDevices;
message = "Unknown devices present in the room"; message = "Unknown devices present in the room";
} }
this.emit(CallEvent.Error, new CallError(code, message, error)); this.emit(CallEvent.Error, new CallError(code, message, <Error>error));
this.terminate(CallParty.Local, code, false); this.terminate(CallParty.Local, code, false);
// no need to carry on & send the candidate queue, but we also // no need to carry on & send the candidate queue, but we also
@ -1734,11 +1722,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
return; // because ICE can still complete as we're ending the call return; // because ICE can still complete as we're ending the call
} }
logger.debug( logger.debug(
"Call ID " + this.callId + ": ICE connection state changed to: " + this.peerConn.iceConnectionState, "Call ID " + this.callId + ": ICE connection state changed to: " + this.peerConn?.iceConnectionState,
); );
// ideally we'd consider the call to be connected when we get media but // ideally we'd consider the call to be connected when we get media but
// chrome doesn't implement any of the 'onstarted' events yet // chrome doesn't implement any of the 'onstarted' events yet
if (this.peerConn.iceConnectionState == 'connected') { if (this.peerConn?.iceConnectionState == 'connected') {
this.setState(CallState.Connected); this.setState(CallState.Connected);
if (!this.callLengthInterval) { if (!this.callLengthInterval) {
@ -1747,16 +1735,13 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.emit(CallEvent.LengthChanged, this.callLength); this.emit(CallEvent.LengthChanged, this.callLength);
}, 1000); }, 1000);
} }
} else if (this.peerConn.iceConnectionState == 'failed') { } else if (this.peerConn?.iceConnectionState == 'failed') {
this.hangup(CallErrorCode.IceFailed, false); this.hangup(CallErrorCode.IceFailed, false);
} }
}; };
private onSignallingStateChanged = (): void => { private onSignallingStateChanged = (): void => {
logger.debug( logger.debug(`call ${this.callId}: Signalling state changed to: ${this.peerConn?.signalingState}`);
"call " + this.callId + ": Signalling state changed to: " +
this.peerConn.signalingState,
);
}; };
private onTrack = (ev: RTCTrackEvent): void => { private onTrack = (ev: RTCTrackEvent): void => {
@ -1796,8 +1781,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// RTCRtpReceiver.getCapabilities and RTCRtpSender.getCapabilities don't seem to be supported on FF // RTCRtpReceiver.getCapabilities and RTCRtpSender.getCapabilities don't seem to be supported on FF
if (!RTCRtpReceiver.getCapabilities || !RTCRtpSender.getCapabilities) return; if (!RTCRtpReceiver.getCapabilities || !RTCRtpSender.getCapabilities) return;
const recvCodecs = RTCRtpReceiver.getCapabilities("video").codecs; const recvCodecs = RTCRtpReceiver.getCapabilities("video")!.codecs;
const sendCodecs = RTCRtpSender.getCapabilities("video").codecs; const sendCodecs = RTCRtpSender.getCapabilities("video")!.codecs;
const codecs = [...sendCodecs, ...recvCodecs]; const codecs = [...sendCodecs, ...recvCodecs];
for (const codec of codecs) { for (const codec of codecs) {
@ -1807,7 +1792,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
} }
for (const trans of this.peerConn.getTransceivers()) { for (const trans of this.peerConn!.getTransceivers()) {
if ( if (
this.screensharingSenders.includes(trans.sender) && this.screensharingSenders.includes(trans.sender) &&
( (
@ -1831,10 +1816,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.makingOffer = true; this.makingOffer = true;
try { try {
this.getRidOfRTXCodecs(); this.getRidOfRTXCodecs();
const myOffer = await this.peerConn.createOffer(); const myOffer = await this.peerConn!.createOffer();
await this.gotLocalOffer(myOffer); await this.gotLocalOffer(myOffer);
} catch (e) { } catch (e) {
this.getLocalOfferFailed(e); this.getLocalOfferFailed(<Error>e);
return; return;
} finally { } finally {
this.makingOffer = false; this.makingOffer = false;
@ -1961,9 +1946,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
* Transfers this call to the target call, effectively 'joining' the * Transfers this call to the target call, effectively 'joining' the
* two calls (so the remote parties on each call are connected together). * two calls (so the remote parties on each call are connected together).
*/ */
public async transferToCall(transferTargetCall?: MatrixCall): Promise<void> { public async transferToCall(transferTargetCall: MatrixCall): Promise<void> {
const targetProfileInfo = await this.client.getProfileInfo(transferTargetCall.getOpponentMember().userId); const targetUserId = transferTargetCall.getOpponentMember()?.userId;
const transfereeProfileInfo = await this.client.getProfileInfo(this.getOpponentMember().userId); const targetProfileInfo = targetUserId ? await this.client.getProfileInfo(targetUserId) : undefined;
const opponentUserId = this.getOpponentMember()?.userId;
const transfereeProfileInfo = opponentUserId ? await this.client.getProfileInfo(opponentUserId) : undefined;
const newCallId = genCallID(); const newCallId = genCallID();
@ -1972,9 +1959,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// ID of the new call (but we can use the same function to generate it) // ID of the new call (but we can use the same function to generate it)
replacement_id: genCallID(), replacement_id: genCallID(),
target_user: { target_user: {
id: this.getOpponentMember().userId, id: opponentUserId,
display_name: transfereeProfileInfo.displayname, display_name: transfereeProfileInfo?.displayname,
avatar_url: transfereeProfileInfo.avatar_url, avatar_url: transfereeProfileInfo?.avatar_url,
}, },
await_call: newCallId, await_call: newCallId,
} as MCallReplacesEvent; } as MCallReplacesEvent;
@ -1984,9 +1971,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const bodyToTransferee = { const bodyToTransferee = {
replacement_id: genCallID(), replacement_id: genCallID(),
target_user: { target_user: {
id: transferTargetCall.getOpponentMember().userId, id: targetUserId,
display_name: targetProfileInfo.displayname, display_name: targetProfileInfo?.displayname,
avatar_url: targetProfileInfo.avatar_url, avatar_url: targetProfileInfo?.avatar_url,
}, },
create_call: newCallId, create_call: newCallId,
} as MCallReplacesEvent; } as MCallReplacesEvent;
@ -2072,7 +2059,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} catch (error) { } catch (error) {
// don't retry this event: we'll send another one later as we might // don't retry this event: we'll send another one later as we might
// have more candidates by then. // have more candidates by then.
if (error.event) this.client.cancelPendingEvent(error.event); if (error instanceof MatrixError && error.event) this.client.cancelPendingEvent(error.event);
// put all the candidates we failed to send back in the queue // put all the candidates we failed to send back in the queue
this.candidateSendQueue.push(...candidates); this.candidateSendQueue.push(...candidates);
@ -2086,7 +2073,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const code = CallErrorCode.SignallingFailed; const code = CallErrorCode.SignallingFailed;
const message = "Signalling failed"; const message = "Signalling failed";
this.emit(CallEvent.Error, new CallError(code, message, error)); this.emit(CallEvent.Error, new CallError(code, message, <Error>error));
this.hangup(code, false); this.hangup(code, false);
return; return;
@ -2123,7 +2110,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const callFeed = new CallFeed({ const callFeed = new CallFeed({
client: this.client, client: this.client,
roomId: this.roomId, roomId: this.roomId,
userId: this.client.getUserId(), userId: this.client.getUserId()!,
stream, stream,
purpose: SDPStreamMetadataPurpose.Usermedia, purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: false, audioMuted: false,
@ -2131,7 +2118,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
}); });
await this.placeCallWithCallFeeds([callFeed]); await this.placeCallWithCallFeeds([callFeed]);
} catch (e) { } catch (e) {
this.getUserMediaFailed(e); this.getUserMediaFailed(<Error>e);
return; return;
} }
} }
@ -2214,12 +2201,12 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
private async addBufferedIceCandidates(): Promise<void> { private async addBufferedIceCandidates(): Promise<void> {
const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId); const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId!);
if (bufferedCandidates) { if (bufferedCandidates) {
logger.info(`Adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`); logger.info(`Adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`);
await this.addIceCandidates(bufferedCandidates); await this.addIceCandidates(bufferedCandidates);
} }
this.remoteCandidateBuffer = null; this.remoteCandidateBuffer.clear();
} }
private async addIceCandidates(candidates: RTCIceCandidate[]): Promise<void> { private async addIceCandidates(candidates: RTCIceCandidate[]): Promise<void> {
@ -2235,7 +2222,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
"Call " + this.callId + " got remote ICE " + candidate.sdpMid + " candidate: " + candidate.candidate, "Call " + this.callId + " got remote ICE " + candidate.sdpMid + " candidate: " + candidate.candidate,
); );
try { try {
await this.peerConn.addIceCandidate(candidate); await this.peerConn!.addIceCandidate(candidate);
} catch (err) { } catch (err) {
if (!this.ignoreOffer) { if (!this.ignoreOffer) {
logger.info("Failed to add remote ICE candidate", err); logger.info("Failed to add remote ICE candidate", err);
@ -2299,7 +2286,11 @@ export function supportsMatrixCall(): boolean {
* since it's only possible to set this option on outbound calls. * since it's only possible to set this option on outbound calls.
* @return {MatrixCall} the call or null if the browser doesn't support calling. * @return {MatrixCall} the call or null if the browser doesn't support calling.
*/ */
export function createNewMatrixCall(client: any, roomId: string, options?: CallOpts): MatrixCall | null { export function createNewMatrixCall(
client: MatrixClient,
roomId: string,
options?: Pick<CallOpts, "forceTURN">,
): MatrixCall | null {
if (!supportsMatrixCall()) return null; if (!supportsMatrixCall()) return null;
const optionsForceTURN = options ? options.forceTURN : false; const optionsForceTURN = options ? options.forceTURN : false;

View File

@ -163,7 +163,7 @@ export class CallEventHandler {
this.client, this.client,
event.getRoomId(), event.getRoomId(),
{ forceTURN: this.client.forceTURN }, { forceTURN: this.client.forceTURN },
); ) ?? undefined;
if (!call) { if (!call) {
logger.log( logger.log(
"Incoming call ID " + content.call_id + " but this client " + "Incoming call ID " + content.call_id + " but this client " +
@ -181,13 +181,13 @@ export class CallEventHandler {
// if we stashed candidate events for that call ID, play them back now // if we stashed candidate events for that call ID, play them back now
if (this.candidateEventsByCall.get(call.callId)) { if (this.candidateEventsByCall.get(call.callId)) {
for (const ev of this.candidateEventsByCall.get(call.callId)) { for (const ev of this.candidateEventsByCall.get(call.callId)!) {
call.onRemoteIceCandidatesReceived(ev); call.onRemoteIceCandidatesReceived(ev);
} }
} }
// Were we trying to call that user (room)? // Were we trying to call that user (room)?
let existingCall: MatrixCall; let existingCall: MatrixCall | undefined;
for (const thisCall of this.calls.values()) { for (const thisCall of this.calls.values()) {
const isCalling = [CallState.WaitLocalMedia, CallState.CreateOffer, CallState.InviteSent].includes( const isCalling = [CallState.WaitLocalMedia, CallState.CreateOffer, CallState.InviteSent].includes(
thisCall.state, thisCall.state,
@ -238,7 +238,7 @@ export class CallEventHandler {
if (!this.candidateEventsByCall.has(content.call_id)) { if (!this.candidateEventsByCall.has(content.call_id)) {
this.candidateEventsByCall.set(content.call_id, []); this.candidateEventsByCall.set(content.call_id, []);
} }
this.candidateEventsByCall.get(content.call_id).push(event); this.candidateEventsByCall.get(content.call_id)!.push(event);
} else { } else {
call.onRemoteIceCandidatesReceived(event); call.onRemoteIceCandidatesReceived(event);
} }
@ -250,7 +250,7 @@ export class CallEventHandler {
// if not live, store the fact that the call has ended because // if not live, store the fact that the call has ended because
// we're probably getting events backwards so // we're probably getting events backwards so
// the hangup will come before the invite // the hangup will come before the invite
call = createNewMatrixCall(this.client, event.getRoomId()); call = createNewMatrixCall(this.client, event.getRoomId()) ?? undefined;
if (call) { if (call) {
call.callId = content.call_id; call.callId = content.call_id;
call.initWithHangup(event); call.initWithHangup(event);

View File

@ -64,12 +64,12 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
private audioMuted: boolean; private audioMuted: boolean;
private videoMuted: boolean; private videoMuted: boolean;
private measuringVolumeActivity = false; private measuringVolumeActivity = false;
private audioContext: AudioContext; private audioContext?: AudioContext;
private analyser: AnalyserNode; private analyser?: AnalyserNode;
private frequencyBinCount: Float32Array; private frequencyBinCount?: Float32Array;
private speakingThreshold = SPEAKING_THRESHOLD; private speakingThreshold = SPEAKING_THRESHOLD;
private speaking = false; private speaking = false;
private volumeLooperTimeout: ReturnType<typeof setTimeout>; private volumeLooperTimeout?: ReturnType<typeof setTimeout>;
constructor(opts: ICallFeedOpts) { constructor(opts: ICallFeedOpts) {
super(); super();
@ -83,6 +83,7 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
this.speakingVolumeSamples = new Array(SPEAKING_SAMPLE_COUNT).fill(-Infinity); this.speakingVolumeSamples = new Array(SPEAKING_SAMPLE_COUNT).fill(-Infinity);
this.updateStream(null, opts.stream); this.updateStream(null, opts.stream);
this.stream = opts.stream; // updateStream does this, but this makes TS happier
if (this.hasAudioTrack) { if (this.hasAudioTrack) {
this.initVolumeMeasuring(); this.initVolumeMeasuring();
@ -93,22 +94,21 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
return this.stream.getAudioTracks().length > 0; return this.stream.getAudioTracks().length > 0;
} }
private updateStream(oldStream: MediaStream, newStream: MediaStream): void { private updateStream(oldStream: MediaStream | null, newStream: MediaStream): void {
if (newStream === oldStream) return; if (newStream === oldStream) return;
if (oldStream) { if (oldStream) {
oldStream.removeEventListener("addtrack", this.onAddTrack); oldStream.removeEventListener("addtrack", this.onAddTrack);
this.measureVolumeActivity(false); this.measureVolumeActivity(false);
} }
if (newStream) {
this.stream = newStream;
newStream.addEventListener("addtrack", this.onAddTrack);
if (this.hasAudioTrack) { this.stream = newStream;
this.initVolumeMeasuring(); newStream.addEventListener("addtrack", this.onAddTrack);
} else {
this.measureVolumeActivity(false); if (this.hasAudioTrack) {
} this.initVolumeMeasuring();
} else {
this.measureVolumeActivity(false);
} }
this.emit(CallFeedEvent.NewStream, this.stream); this.emit(CallFeedEvent.NewStream, this.stream);
@ -138,9 +138,9 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
* Returns callRoom member * Returns callRoom member
* @returns member of the callRoom * @returns member of the callRoom
*/ */
public getMember(): RoomMember { public getMember(): RoomMember | null {
const callRoom = this.client.getRoom(this.roomId); const callRoom = this.client.getRoom(this.roomId);
return callRoom.getMember(this.userId); return callRoom?.getMember(this.userId) ?? null;
} }
/** /**
@ -177,9 +177,10 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
/** /**
* Set one or both of feed's internal audio and video video mute state * Set one or both of feed's internal audio and video video mute state
* Either value may be null to leave it as-is * Either value may be null to leave it as-is
* @param muted is the feed's video muted? * @param audioMuted is the feed's audio muted?
* @param videoMuted is the feed's video muted?
*/ */
public setAudioVideoMuted(audioMuted: boolean, videoMuted: boolean): void { public setAudioVideoMuted(audioMuted: boolean | null, videoMuted: boolean | null): void {
if (audioMuted !== null) { if (audioMuted !== null) {
if (this.audioMuted !== audioMuted) { if (this.audioMuted !== audioMuted) {
this.speakingVolumeSamples.fill(-Infinity); this.speakingVolumeSamples.fill(-Infinity);
@ -216,12 +217,12 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
if (!this.measuringVolumeActivity) return; if (!this.measuringVolumeActivity) return;
this.analyser.getFloatFrequencyData(this.frequencyBinCount); this.analyser.getFloatFrequencyData(this.frequencyBinCount!);
let maxVolume = -Infinity; let maxVolume = -Infinity;
for (let i = 0; i < this.frequencyBinCount.length; i++) { for (let i = 0; i < this.frequencyBinCount!.length; i++) {
if (this.frequencyBinCount[i] > maxVolume) { if (this.frequencyBinCount![i] > maxVolume) {
maxVolume = this.frequencyBinCount[i]; maxVolume = this.frequencyBinCount![i];
} }
} }

View File

@ -22,8 +22,8 @@ import { MatrixClient } from "../client";
import { CallState } from "./call"; import { CallState } from "./call";
export class MediaHandler { export class MediaHandler {
private audioInput: string; private audioInput?: string;
private videoInput: string; private videoInput?: string;
private localUserMediaStream?: MediaStream; private localUserMediaStream?: MediaStream;
public userMediaStreams: MediaStream[] = []; public userMediaStreams: MediaStream[] = [];
public screensharingStreams: MediaStream[] = []; public screensharingStreams: MediaStream[] = [];
@ -75,7 +75,7 @@ export class MediaHandler {
for (const call of this.client.callEventHandler.calls.values()) { for (const call of this.client.callEventHandler.calls.values()) {
if (call.state === CallState.Ended || !callMediaStreamParams.has(call.callId)) continue; if (call.state === CallState.Ended || !callMediaStreamParams.has(call.callId)) continue;
const { audio, video } = callMediaStreamParams.get(call.callId); const { audio, video } = callMediaStreamParams.get(call.callId)!;
// This stream won't be reusable as we will replace the tracks of the old stream // This stream won't be reusable as we will replace the tracks of the old stream
const stream = await this.getUserMediaStream(audio, video, false); const stream = await this.getUserMediaStream(audio, video, false);
@ -121,9 +121,9 @@ export class MediaHandler {
const settings = track.getSettings(); const settings = track.getSettings();
if (track.kind === "audio") { if (track.kind === "audio") {
this.audioInput = settings.deviceId; this.audioInput = settings.deviceId!;
} else if (track.kind === "video") { } else if (track.kind === "video") {
this.videoInput = settings.deviceId; this.videoInput = settings.deviceId!;
} }
} }
@ -179,7 +179,10 @@ export class MediaHandler {
* @param reusable is allowed to be reused by the MediaHandler * @param reusable is allowed to be reused by the MediaHandler
* @returns {MediaStream} based on passed parameters * @returns {MediaStream} based on passed parameters
*/ */
public async getScreensharingStream(desktopCapturerSourceId: string, reusable = true): Promise<MediaStream | null> { public async getScreensharingStream(
desktopCapturerSourceId?: string,
reusable = true,
): Promise<MediaStream | null> {
let stream: MediaStream; let stream: MediaStream;
if (this.screensharingStreams.length === 0) { if (this.screensharingStreams.length === 0) {