You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-30 04:23:07 +03:00
Merge branch 'develop' into madlittlemods/stablize-msc3030-timestamp-to-event
This commit is contained in:
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@ -32,9 +32,10 @@ jobs:
|
|||||||
ref: gh-pages
|
ref: gh-pages
|
||||||
|
|
||||||
- name: 🔪 Prepare
|
- name: 🔪 Prepare
|
||||||
|
env:
|
||||||
|
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
tag="${{ github.ref_name }}"
|
VERSION="${GITHUB_REF_NAME#v}"
|
||||||
VERSION="${tag#v}"
|
|
||||||
[ ! -e "$VERSION" ] || rm -r $VERSION
|
[ ! -e "$VERSION" ] || rm -r $VERSION
|
||||||
cp -r $RUNNER_TEMP/_docs/ $VERSION
|
cp -r $RUNNER_TEMP/_docs/ $VERSION
|
||||||
|
|
||||||
|
@ -21,3 +21,6 @@ out
|
|||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
|
||||||
|
/CHANGELOG.md
|
||||||
|
7789
CHANGELOG.md
7789
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
|||||||
To try it out, **you must build the SDK first** and then host this folder:
|
To try it out, **you must build the SDK first** and then host this folder:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
$ yarn install
|
||||||
$ yarn build
|
$ yarn build
|
||||||
$ cd examples/browser
|
$ cd examples/browser
|
||||||
$ python -m http.server 8003
|
$ python -m http.server 8003
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-js-sdk",
|
"name": "matrix-js-sdk",
|
||||||
"version": "22.0.0",
|
"version": "23.0.0",
|
||||||
"description": "Matrix Client-Server SDK for Javascript",
|
"description": "Matrix Client-Server SDK for Javascript",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
|
@ -21,9 +21,9 @@ if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
|||||||
# to the TypeScript source.
|
# to the TypeScript source.
|
||||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||||
if [ "$src_value" != "null" ]; then
|
if [ "$src_value" != "null" ]; then
|
||||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
|
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||||
else
|
else
|
||||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
|
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
@ -184,7 +184,7 @@ for i in main typings
|
|||||||
do
|
do
|
||||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||||
if [ "$lib_value" != "null" ]; then
|
if [ "$lib_value" != "null" ]; then
|
||||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
|
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ import MockHttpBackend from "matrix-mock-request";
|
|||||||
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
|
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
|
||||||
import { logger } from "../src/logger";
|
import { logger } from "../src/logger";
|
||||||
import { syncPromise } from "./test-utils/test-utils";
|
import { syncPromise } from "./test-utils/test-utils";
|
||||||
import { createClient } from "../src/matrix";
|
import { createClient, IStartClientOpts } from "../src/matrix";
|
||||||
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
|
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
|
||||||
import { MockStorageApi } from "./MockStorageApi";
|
import { MockStorageApi } from "./MockStorageApi";
|
||||||
import { encodeUri } from "../src/utils";
|
import { encodeUri } from "../src/utils";
|
||||||
@ -79,9 +79,12 @@ export class TestClient {
|
|||||||
/**
|
/**
|
||||||
* start the client, and wait for it to initialise.
|
* start the client, and wait for it to initialise.
|
||||||
*/
|
*/
|
||||||
public start(): Promise<void> {
|
public start(opts: IStartClientOpts = {}): Promise<void> {
|
||||||
logger.log(this + ": starting");
|
logger.log(this + ": starting");
|
||||||
this.httpBackend.when("GET", "/versions").respond(200, {});
|
this.httpBackend.when("GET", "/versions").respond(200, {
|
||||||
|
// we have tests that rely on support for lazy-loading members
|
||||||
|
versions: ["r0.5.0"],
|
||||||
|
});
|
||||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||||
this.expectDeviceKeyUpload();
|
this.expectDeviceKeyUpload();
|
||||||
@ -93,6 +96,8 @@ export class TestClient {
|
|||||||
this.client.startClient({
|
this.client.startClient({
|
||||||
// set this so that we can get hold of failed events
|
// set this so that we can get hold of failed events
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
|
||||||
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => {
|
return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => {
|
||||||
|
@ -1016,6 +1016,61 @@ describe("MatrixClient event timelines", function () {
|
|||||||
httpBackend.flushAllExpected(),
|
httpBackend.flushAllExpected(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should create threads for thread roots discovered", function () {
|
||||||
|
const room = client.getRoom(roomId)!;
|
||||||
|
const timelineSet = room.getTimelineSets()[0];
|
||||||
|
|
||||||
|
httpBackend
|
||||||
|
.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id!))
|
||||||
|
.respond(200, function () {
|
||||||
|
return {
|
||||||
|
start: "start_token0",
|
||||||
|
events_before: [],
|
||||||
|
event: EVENTS[0],
|
||||||
|
events_after: [],
|
||||||
|
end: "end_token0",
|
||||||
|
state: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
httpBackend
|
||||||
|
.when("GET", "/rooms/!foo%3Abar/messages")
|
||||||
|
.check(function (req) {
|
||||||
|
const params = req.queryParams!;
|
||||||
|
expect(params.dir).toEqual("b");
|
||||||
|
expect(params.from).toEqual("start_token0");
|
||||||
|
expect(params.limit).toEqual("30");
|
||||||
|
})
|
||||||
|
.respond(200, function () {
|
||||||
|
return {
|
||||||
|
chunk: [EVENTS[1], EVENTS[2], THREAD_ROOT],
|
||||||
|
end: "start_token1",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let tl: EventTimeline;
|
||||||
|
return Promise.all([
|
||||||
|
client
|
||||||
|
.getEventTimeline(timelineSet, EVENTS[0].event_id!)
|
||||||
|
.then(function (tl0) {
|
||||||
|
tl = tl0!;
|
||||||
|
return client.paginateEventTimeline(tl, { backwards: true });
|
||||||
|
})
|
||||||
|
.then(function (success) {
|
||||||
|
expect(success).toBeTruthy();
|
||||||
|
expect(tl!.getEvents().length).toEqual(4);
|
||||||
|
expect(tl!.getEvents()[0].event).toEqual(THREAD_ROOT);
|
||||||
|
expect(tl!.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||||
|
expect(tl!.getEvents()[2].event).toEqual(EVENTS[1]);
|
||||||
|
expect(tl!.getEvents()[3].event).toEqual(EVENTS[0]);
|
||||||
|
expect(room.getThreads().map((it) => it.id)).toEqual([THREAD_ROOT.event_id!]);
|
||||||
|
expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("start_token1");
|
||||||
|
expect(tl!.getPaginationToken(EventTimeline.FORWARDS)).toEqual("end_token0");
|
||||||
|
}),
|
||||||
|
httpBackend.flushAllExpected(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should ensure thread events are ordered correctly", async () => {
|
it("should ensure thread events are ordered correctly", async () => {
|
||||||
|
@ -35,9 +35,7 @@ describe("MatrixClient", function () {
|
|||||||
let store: MemoryStore | undefined;
|
let store: MemoryStore | undefined;
|
||||||
|
|
||||||
const defaultClientOpts: IStoredClientOpts = {
|
const defaultClientOpts: IStoredClientOpts = {
|
||||||
canResetEntireTimeline: (roomId) => false,
|
|
||||||
experimentalThreadSupport: false,
|
experimentalThreadSupport: false,
|
||||||
crypto: {} as unknown as IStoredClientOpts["crypto"],
|
|
||||||
};
|
};
|
||||||
const setupTests = (): [MatrixClient, HttpBackend, MemoryStore] => {
|
const setupTests = (): [MatrixClient, HttpBackend, MemoryStore] => {
|
||||||
const store = new MemoryStore();
|
const store = new MemoryStore();
|
||||||
|
@ -1543,6 +1543,52 @@ describe("MatrixClient syncing", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("only replays receipts relevant to the current context", async () => {
|
||||||
|
const THREAD_ID = "$unknownthread:localhost";
|
||||||
|
|
||||||
|
const receipt = {
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
content: {
|
||||||
|
"$event1:localhost": {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
"@alice:localhost": { ts: 666, thread_id: THREAD_ID },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"$otherevent:localhost": {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
"@alice:localhost": { ts: 999, thread_id: "$otherthread:localhost" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
syncData.rooms.join[roomOne].ephemeral.events = [receipt];
|
||||||
|
|
||||||
|
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||||
|
client!.startClient();
|
||||||
|
|
||||||
|
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]).then(() => {
|
||||||
|
const room = client?.getRoom(roomOne);
|
||||||
|
expect(room).toBeInstanceOf(Room);
|
||||||
|
|
||||||
|
expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(true);
|
||||||
|
|
||||||
|
const thread = room!.createThread(THREAD_ID, undefined, [], true);
|
||||||
|
|
||||||
|
expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(false);
|
||||||
|
|
||||||
|
const receipt = thread.getReadReceiptForUserId("@alice:localhost");
|
||||||
|
|
||||||
|
expect(receipt).toStrictEqual({
|
||||||
|
data: {
|
||||||
|
thread_id: "$unknownthread:localhost",
|
||||||
|
ts: 666,
|
||||||
|
},
|
||||||
|
eventId: "$event1:localhost",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("of a room", () => {
|
describe("of a room", () => {
|
||||||
|
@ -1590,4 +1590,92 @@ describe("megolm", () => {
|
|||||||
aliceTestClient.httpBackend.flush("/send/m.room.encrypted/", 1),
|
aliceTestClient.httpBackend.flush("/send/m.room.encrypted/", 1),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Lazy-loading member lists", () => {
|
||||||
|
let p2pSession: Olm.Session;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// set up the aliceTestClient so that it is a room with no known members
|
||||||
|
aliceTestClient.expectKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||||
|
await aliceTestClient.start({ lazyLoadMembers: true });
|
||||||
|
aliceTestClient.client.setGlobalErrorOnUnknownDevices(false);
|
||||||
|
|
||||||
|
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse([]));
|
||||||
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
|
p2pSession = await establishOlmSession(aliceTestClient, testOlmAccount);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function expectMembershipRequest(roomId: string, members: string[]): Promise<void> {
|
||||||
|
const membersPath = `/rooms/${encodeURIComponent(roomId)}/members?not_membership=leave`;
|
||||||
|
aliceTestClient.httpBackend.when("GET", membersPath).respond(200, {
|
||||||
|
chunk: [
|
||||||
|
testUtils.mkMembershipCustom({
|
||||||
|
membership: "join",
|
||||||
|
sender: "@bob:xyz",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await aliceTestClient.httpBackend.flush(membersPath, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("Sending an event initiates a member list sync", async () => {
|
||||||
|
// we expect a call to the /members list...
|
||||||
|
const memberListPromise = expectMembershipRequest(ROOM_ID, ["@bob:xyz"]);
|
||||||
|
|
||||||
|
// then a request for bob's devices...
|
||||||
|
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz"));
|
||||||
|
|
||||||
|
// then a to-device with the room_key
|
||||||
|
const inboundGroupSessionPromise = expectSendRoomKey(
|
||||||
|
aliceTestClient.httpBackend,
|
||||||
|
"@bob:xyz",
|
||||||
|
testOlmAccount,
|
||||||
|
p2pSession,
|
||||||
|
);
|
||||||
|
|
||||||
|
// and finally the megolm message
|
||||||
|
const megolmMessagePromise = expectSendMegolmMessage(
|
||||||
|
aliceTestClient.httpBackend,
|
||||||
|
inboundGroupSessionPromise,
|
||||||
|
);
|
||||||
|
|
||||||
|
// kick it off
|
||||||
|
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, "test");
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
sendPromise,
|
||||||
|
megolmMessagePromise,
|
||||||
|
memberListPromise,
|
||||||
|
aliceTestClient.httpBackend.flush("/keys/query", 1),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loading the membership list inhibits a later load", async () => {
|
||||||
|
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||||
|
await Promise.all([room.loadMembersIfNeeded(), expectMembershipRequest(ROOM_ID, ["@bob:xyz"])]);
|
||||||
|
|
||||||
|
// expect a request for bob's devices...
|
||||||
|
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz"));
|
||||||
|
|
||||||
|
// then a to-device with the room_key
|
||||||
|
const inboundGroupSessionPromise = expectSendRoomKey(
|
||||||
|
aliceTestClient.httpBackend,
|
||||||
|
"@bob:xyz",
|
||||||
|
testOlmAccount,
|
||||||
|
p2pSession,
|
||||||
|
);
|
||||||
|
|
||||||
|
// and finally the megolm message
|
||||||
|
const megolmMessagePromise = expectSendMegolmMessage(
|
||||||
|
aliceTestClient.httpBackend,
|
||||||
|
inboundGroupSessionPromise,
|
||||||
|
);
|
||||||
|
|
||||||
|
// kick it off
|
||||||
|
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, "test");
|
||||||
|
|
||||||
|
await Promise.all([sendPromise, megolmMessagePromise, aliceTestClient.httpBackend.flush("/keys/query", 1)]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -38,7 +38,7 @@ import {
|
|||||||
IRoomTimelineData,
|
IRoomTimelineData,
|
||||||
} from "../../src";
|
} from "../../src";
|
||||||
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
|
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
|
||||||
import { SyncState } from "../../src/sync";
|
import { SyncApiOptions, SyncState } from "../../src/sync";
|
||||||
import { IStoredClientOpts } from "../../src/client";
|
import { IStoredClientOpts } from "../../src/client";
|
||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
import { emitPromise } from "../test-utils/test-utils";
|
import { emitPromise } from "../test-utils/test-utils";
|
||||||
@ -111,6 +111,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
// assign client/httpBackend globals
|
// assign client/httpBackend globals
|
||||||
const setupClient = async (testOpts?: Partial<IStoredClientOpts & { withCrypto: boolean }>) => {
|
const setupClient = async (testOpts?: Partial<IStoredClientOpts & { withCrypto: boolean }>) => {
|
||||||
testOpts = testOpts || {};
|
testOpts = testOpts || {};
|
||||||
|
const syncOpts: SyncApiOptions = {};
|
||||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||||
httpBackend = testClient.httpBackend;
|
httpBackend = testClient.httpBackend;
|
||||||
client = testClient.client;
|
client = testClient.client;
|
||||||
@ -118,10 +119,10 @@ describe("SlidingSyncSdk", () => {
|
|||||||
if (testOpts.withCrypto) {
|
if (testOpts.withCrypto) {
|
||||||
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||||
await client!.initCrypto();
|
await client!.initCrypto();
|
||||||
testOpts.crypto = client!.crypto;
|
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
|
||||||
}
|
}
|
||||||
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
||||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
|
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
|
||||||
};
|
};
|
||||||
|
|
||||||
// tear down client/httpBackend globals
|
// tear down client/httpBackend globals
|
||||||
|
@ -1418,6 +1418,102 @@ describe("SlidingSync", () => {
|
|||||||
await httpBackend!.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
slidingSync.stop();
|
slidingSync.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not be possible to add/modify an already added custom subscription", async () => {
|
||||||
|
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||||
|
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||||
|
slidingSync.addCustomSubscription(customSubName1, customSub2);
|
||||||
|
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||||
|
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||||
|
|
||||||
|
httpBackend!
|
||||||
|
.when("POST", syncUrl)
|
||||||
|
.check(function (req) {
|
||||||
|
const body = req.data;
|
||||||
|
logger.log("custom subs", body);
|
||||||
|
expect(body.room_subscriptions).toBeTruthy();
|
||||||
|
expect(body.room_subscriptions[roomA]).toEqual(customSub1);
|
||||||
|
})
|
||||||
|
.respond(200, {
|
||||||
|
pos: "b",
|
||||||
|
lists: [],
|
||||||
|
extensions: {},
|
||||||
|
rooms: {},
|
||||||
|
});
|
||||||
|
slidingSync.start();
|
||||||
|
await httpBackend!.flushAllExpected();
|
||||||
|
slidingSync.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should change the custom subscription if they are different", async () => {
|
||||||
|
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||||
|
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||||
|
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||||
|
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||||
|
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||||
|
|
||||||
|
httpBackend!
|
||||||
|
.when("POST", syncUrl)
|
||||||
|
.check(function (req) {
|
||||||
|
const body = req.data;
|
||||||
|
logger.log("custom subs", body);
|
||||||
|
expect(body.room_subscriptions).toBeTruthy();
|
||||||
|
expect(body.room_subscriptions[roomA]).toEqual(customSub1);
|
||||||
|
expect(body.unsubscribe_rooms).toBeUndefined();
|
||||||
|
})
|
||||||
|
.respond(200, {
|
||||||
|
pos: "b",
|
||||||
|
lists: [],
|
||||||
|
extensions: {},
|
||||||
|
rooms: {},
|
||||||
|
});
|
||||||
|
slidingSync.start();
|
||||||
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
|
// using the same subscription doesn't unsub nor changes subscriptions
|
||||||
|
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||||
|
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||||
|
|
||||||
|
httpBackend!
|
||||||
|
.when("POST", syncUrl)
|
||||||
|
.check(function (req) {
|
||||||
|
const body = req.data;
|
||||||
|
logger.log("custom subs", body);
|
||||||
|
expect(body.room_subscriptions).toBeUndefined();
|
||||||
|
expect(body.unsubscribe_rooms).toBeUndefined();
|
||||||
|
})
|
||||||
|
.respond(200, {
|
||||||
|
pos: "b",
|
||||||
|
lists: [],
|
||||||
|
extensions: {},
|
||||||
|
rooms: {},
|
||||||
|
});
|
||||||
|
slidingSync.start();
|
||||||
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
|
// Changing the subscription works
|
||||||
|
slidingSync.useCustomSubscription(roomA, customSubName2);
|
||||||
|
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||||
|
|
||||||
|
httpBackend!
|
||||||
|
.when("POST", syncUrl)
|
||||||
|
.check(function (req) {
|
||||||
|
const body = req.data;
|
||||||
|
logger.log("custom subs", body);
|
||||||
|
expect(body.room_subscriptions).toBeTruthy();
|
||||||
|
expect(body.room_subscriptions[roomA]).toEqual(customSub2);
|
||||||
|
expect(body.unsubscribe_rooms).toBeUndefined();
|
||||||
|
})
|
||||||
|
.respond(200, {
|
||||||
|
pos: "b",
|
||||||
|
lists: [],
|
||||||
|
extensions: {},
|
||||||
|
rooms: {},
|
||||||
|
});
|
||||||
|
slidingSync.start();
|
||||||
|
await httpBackend!.flushAllExpected();
|
||||||
|
slidingSync.stop();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("extensions", () => {
|
describe("extensions", () => {
|
||||||
|
@ -513,9 +513,6 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
|
|||||||
|
|
||||||
public sendMetadataUpdate = jest.fn<void, []>();
|
public sendMetadataUpdate = jest.fn<void, []>();
|
||||||
|
|
||||||
public on = jest.fn();
|
|
||||||
public removeListener = jest.fn();
|
|
||||||
|
|
||||||
public getOpponentMember(): Partial<RoomMember> {
|
public getOpponentMember(): Partial<RoomMember> {
|
||||||
return this.opponentMember;
|
return this.opponentMember;
|
||||||
}
|
}
|
||||||
|
@ -544,7 +544,7 @@ describe("AutoDiscovery", function () {
|
|||||||
.respond(200, {
|
.respond(200, {
|
||||||
versions: ["r0.0.1"],
|
versions: ["r0.0.1"],
|
||||||
});
|
});
|
||||||
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(404, {});
|
httpBackend.when("GET", "/_matrix/identity/v2").respond(404, {});
|
||||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||||
"m.homeserver": {
|
"m.homeserver": {
|
||||||
// Note: we also expect this test to trim the trailing slash
|
// Note: we also expect this test to trim the trailing slash
|
||||||
@ -591,7 +591,7 @@ describe("AutoDiscovery", function () {
|
|||||||
.respond(200, {
|
.respond(200, {
|
||||||
versions: ["r0.0.1"],
|
versions: ["r0.0.1"],
|
||||||
});
|
});
|
||||||
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(500, {});
|
httpBackend.when("GET", "/_matrix/identity/v2").respond(500, {});
|
||||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||||
"m.homeserver": {
|
"m.homeserver": {
|
||||||
// Note: we also expect this test to trim the trailing slash
|
// Note: we also expect this test to trim the trailing slash
|
||||||
@ -636,9 +636,9 @@ describe("AutoDiscovery", function () {
|
|||||||
versions: ["r0.0.1"],
|
versions: ["r0.0.1"],
|
||||||
});
|
});
|
||||||
httpBackend
|
httpBackend
|
||||||
.when("GET", "/_matrix/identity/api/v1")
|
.when("GET", "/_matrix/identity/v2")
|
||||||
.check((req) => {
|
.check((req) => {
|
||||||
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/v2");
|
||||||
})
|
})
|
||||||
.respond(200, {});
|
.respond(200, {});
|
||||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||||
@ -682,9 +682,9 @@ describe("AutoDiscovery", function () {
|
|||||||
versions: ["r0.0.1"],
|
versions: ["r0.0.1"],
|
||||||
});
|
});
|
||||||
httpBackend
|
httpBackend
|
||||||
.when("GET", "/_matrix/identity/api/v1")
|
.when("GET", "/_matrix/identity/v2")
|
||||||
.check((req) => {
|
.check((req) => {
|
||||||
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/v2");
|
||||||
})
|
})
|
||||||
.respond(200, {});
|
.respond(200, {});
|
||||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||||
|
@ -167,6 +167,38 @@ describe("Crypto", function () {
|
|||||||
|
|
||||||
client.stopClient();
|
client.stopClient();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("doesn't throw an error when attempting to decrypt a redacted event", async () => {
|
||||||
|
const client = new TestClient("@alice:example.com", "deviceid").client;
|
||||||
|
await client.initCrypto();
|
||||||
|
|
||||||
|
const event = new MatrixEvent({
|
||||||
|
content: {},
|
||||||
|
event_id: "$event_id",
|
||||||
|
room_id: "!room_id",
|
||||||
|
sender: "@bob:example.com",
|
||||||
|
type: "m.room.encrypted",
|
||||||
|
unsigned: {
|
||||||
|
redacted_because: {
|
||||||
|
content: {},
|
||||||
|
event_id: "$redaction_event_id",
|
||||||
|
redacts: "$event_id",
|
||||||
|
room_id: "!room_id",
|
||||||
|
origin_server_ts: 1234567890,
|
||||||
|
sender: "@bob:example.com",
|
||||||
|
type: "m.room.redaction",
|
||||||
|
unsigned: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await event.attemptDecryption(client.crypto!);
|
||||||
|
expect(event.isDecryptionFailure()).toBeFalsy();
|
||||||
|
// since the redaction event isn't encrypted, the redacted_because
|
||||||
|
// should be the same as in the original event
|
||||||
|
expect(event.getRedactionEvent()).toEqual(event.getUnsigned().redacted_because);
|
||||||
|
|
||||||
|
client.stopClient();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Session management", function () {
|
describe("Session management", function () {
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
/*
|
|
||||||
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 "../../../olm-loader";
|
|
||||||
|
|
||||||
import { CRYPTO_ENABLED, MatrixClient } from "../../../../src/client";
|
|
||||||
import { TestClient } from "../../../TestClient";
|
|
||||||
|
|
||||||
const Olm = global.Olm;
|
|
||||||
|
|
||||||
describe("crypto.setDeviceVerification", () => {
|
|
||||||
const userId = "@alice:example.com";
|
|
||||||
const deviceId1 = "device1";
|
|
||||||
let client: MatrixClient;
|
|
||||||
|
|
||||||
if (!CRYPTO_ENABLED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await Olm.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
client = new TestClient(userId, deviceId1).client;
|
|
||||||
await client.initCrypto();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("client should provide crypto", () => {
|
|
||||||
expect(client.crypto).not.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when setting an own device as verified", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.spyOn(client.crypto!, "cancelAndResendAllOutgoingKeyRequests");
|
|
||||||
await client.crypto!.setDeviceVerification(userId, deviceId1, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cancelAndResendAllOutgoingKeyRequests should be called", () => {
|
|
||||||
expect(client.crypto!.cancelAndResendAllOutgoingKeyRequests).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -24,10 +24,13 @@ import {
|
|||||||
MatrixClient,
|
MatrixClient,
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
MatrixEventEvent,
|
MatrixEventEvent,
|
||||||
|
RelationType,
|
||||||
Room,
|
Room,
|
||||||
|
RoomEvent,
|
||||||
} from "../../src";
|
} from "../../src";
|
||||||
import { Thread } from "../../src/models/thread";
|
import { FeatureSupport, Thread } from "../../src/models/thread";
|
||||||
import { ReEmitter } from "../../src/ReEmitter";
|
import { ReEmitter } from "../../src/ReEmitter";
|
||||||
|
import { eventMapperFor } from "../../src/event-mapper";
|
||||||
|
|
||||||
describe("EventTimelineSet", () => {
|
describe("EventTimelineSet", () => {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
@ -202,6 +205,88 @@ describe("EventTimelineSet", () => {
|
|||||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should allow edits to be added to thread timeline", async () => {
|
||||||
|
jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
|
||||||
|
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
|
||||||
|
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||||
|
|
||||||
|
const sender = "@alice:matrix.org";
|
||||||
|
|
||||||
|
const root = utils.mkEvent({
|
||||||
|
event: true,
|
||||||
|
content: {
|
||||||
|
body: "Thread root",
|
||||||
|
},
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender,
|
||||||
|
});
|
||||||
|
room.addLiveEvents([root]);
|
||||||
|
|
||||||
|
const threadReply = utils.mkEvent({
|
||||||
|
event: true,
|
||||||
|
content: {
|
||||||
|
"body": "Thread reply",
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: root.getId()!,
|
||||||
|
rel_type: RelationType.Thread,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender,
|
||||||
|
});
|
||||||
|
|
||||||
|
root.setUnsigned({
|
||||||
|
"m.relations": {
|
||||||
|
[RelationType.Thread]: {
|
||||||
|
count: 1,
|
||||||
|
latest_event: {
|
||||||
|
content: threadReply.getContent(),
|
||||||
|
origin_server_ts: 5,
|
||||||
|
room_id: room.roomId,
|
||||||
|
sender,
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
event_id: threadReply.getId()!,
|
||||||
|
user_id: sender,
|
||||||
|
age: 1,
|
||||||
|
},
|
||||||
|
current_user_participated: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const editToThreadReply = utils.mkEvent({
|
||||||
|
event: true,
|
||||||
|
content: {
|
||||||
|
"body": " * edit",
|
||||||
|
"m.new_content": {
|
||||||
|
"body": "edit",
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"org.matrix.msc1767.text": "edit",
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: threadReply.getId()!,
|
||||||
|
rel_type: RelationType.Replace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender,
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(client, "paginateEventTimeline").mockImplementation(async () => {
|
||||||
|
thread.timelineSet.getLiveTimeline().addEvent(threadReply, { toStartOfTimeline: true });
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
jest.spyOn(client, "relations").mockResolvedValue({
|
||||||
|
events: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const thread = room.createThread(root.getId()!, root, [threadReply, editToThreadReply], false);
|
||||||
|
thread.once(RoomEvent.TimelineReset, () => {
|
||||||
|
const lastEvent = thread.timeline.at(-1)!;
|
||||||
|
expect(lastEvent.getContent().body).toBe(" * edit");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("non-room timeline", () => {
|
describe("non-room timeline", () => {
|
||||||
it("Adds event to timeline", () => {
|
it("Adds event to timeline", () => {
|
||||||
const nonRoomEventTimelineSet = new EventTimelineSet(
|
const nonRoomEventTimelineSet = new EventTimelineSet(
|
||||||
|
@ -22,11 +22,13 @@ import { Filter } from "../../src/filter";
|
|||||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
|
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
|
||||||
import {
|
import {
|
||||||
EventType,
|
EventType,
|
||||||
|
RelationType,
|
||||||
RoomCreateTypeField,
|
RoomCreateTypeField,
|
||||||
RoomType,
|
RoomType,
|
||||||
UNSTABLE_MSC3088_ENABLED,
|
UNSTABLE_MSC3088_ENABLED,
|
||||||
UNSTABLE_MSC3088_PURPOSE,
|
UNSTABLE_MSC3088_PURPOSE,
|
||||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||||
|
MSC3912_RELATION_BASED_REDACTIONS_PROP,
|
||||||
} from "../../src/@types/event";
|
} from "../../src/@types/event";
|
||||||
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
||||||
import { Crypto } from "../../src/crypto";
|
import { Crypto } from "../../src/crypto";
|
||||||
@ -170,6 +172,10 @@ describe("MatrixClient", function () {
|
|||||||
data: SYNC_DATA,
|
data: SYNC_DATA,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const unstableFeatures: Record<string, boolean> = {
|
||||||
|
"org.matrix.msc3440.stable": true,
|
||||||
|
};
|
||||||
|
|
||||||
// items are popped off when processed and block if no items left.
|
// items are popped off when processed and block if no items left.
|
||||||
let httpLookups: HttpLookup[] = [];
|
let httpLookups: HttpLookup[] = [];
|
||||||
let acceptKeepalives: boolean;
|
let acceptKeepalives: boolean;
|
||||||
@ -188,9 +194,7 @@ describe("MatrixClient", function () {
|
|||||||
const { prefix } = requestOpts;
|
const { prefix } = requestOpts;
|
||||||
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
|
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
unstable_features: {
|
unstable_features: unstableFeatures,
|
||||||
"org.matrix.msc3440.stable": true,
|
|
||||||
},
|
|
||||||
versions: ["r0.6.0", "r0.6.1"],
|
versions: ["r0.6.0", "r0.6.1"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1314,6 +1318,59 @@ describe("MatrixClient", function () {
|
|||||||
|
|
||||||
await client.redactEvent(roomId, eventId, txnId, { reason });
|
await client.redactEvent(roomId, eventId, txnId, { reason });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when calling with with_relations", () => {
|
||||||
|
const eventId = "$event42:example.org";
|
||||||
|
|
||||||
|
it("should raise an error if server has no support for relation based redactions", async () => {
|
||||||
|
// load supported features
|
||||||
|
await client.getVersions();
|
||||||
|
|
||||||
|
const txnId = client.makeTxnId();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
client.redactEvent(roomId, eventId, txnId, {
|
||||||
|
with_relations: [RelationType.Reference],
|
||||||
|
});
|
||||||
|
}).toThrowError(
|
||||||
|
new Error(
|
||||||
|
"Server does not support relation based redactions " +
|
||||||
|
`roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId null`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and the server supports relation based redactions (unstable)", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
unstableFeatures["org.matrix.msc3912"] = true;
|
||||||
|
// load supported features
|
||||||
|
await client.getVersions();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should send with_relations in the request body", async () => {
|
||||||
|
const txnId = client.makeTxnId();
|
||||||
|
|
||||||
|
httpLookups = [
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
path:
|
||||||
|
`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}` +
|
||||||
|
`/${encodeURIComponent(txnId)}`,
|
||||||
|
expectBody: {
|
||||||
|
reason: "redaction test",
|
||||||
|
[MSC3912_RELATION_BASED_REDACTIONS_PROP.unstable!]: [RelationType.Reference],
|
||||||
|
},
|
||||||
|
data: { event_id: eventId },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await client.redactEvent(roomId, eventId, txnId, {
|
||||||
|
reason: "redaction test",
|
||||||
|
with_relations: [RelationType.Reference],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cancelPendingEvent", () => {
|
describe("cancelPendingEvent", () => {
|
||||||
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||||||
|
|
||||||
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
|
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
|
||||||
import { emitPromise } from "../../test-utils/test-utils";
|
import { emitPromise } from "../../test-utils/test-utils";
|
||||||
import { EventType } from "../../../src";
|
|
||||||
import { Crypto } from "../../../src/crypto";
|
import { Crypto } from "../../../src/crypto";
|
||||||
|
|
||||||
describe("MatrixEvent", () => {
|
describe("MatrixEvent", () => {
|
||||||
@ -88,22 +87,6 @@ describe("MatrixEvent", () => {
|
|||||||
expect(ev.getWireContent().ciphertext).toBeUndefined();
|
expect(ev.getWireContent().ciphertext).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should abort decryption if fails with an error other than a DecryptionError", async () => {
|
|
||||||
const ev = new MatrixEvent({
|
|
||||||
type: EventType.RoomMessageEncrypted,
|
|
||||||
content: {
|
|
||||||
body: "Test",
|
|
||||||
},
|
|
||||||
event_id: "$event1:server",
|
|
||||||
});
|
|
||||||
await ev.attemptDecryption({
|
|
||||||
decryptEvent: jest.fn().mockRejectedValue(new Error("Not a DecryptionError")),
|
|
||||||
} as unknown as Crypto);
|
|
||||||
expect(ev.isEncrypted()).toBeTruthy();
|
|
||||||
expect(ev.isBeingDecrypted()).toBeFalsy();
|
|
||||||
expect(ev.isDecryptionFailure()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("applyVisibilityEvent", () => {
|
describe("applyVisibilityEvent", () => {
|
||||||
it("should emit VisibilityChange if a change was made", async () => {
|
it("should emit VisibilityChange if a change was made", async () => {
|
||||||
const ev = new MatrixEvent({
|
const ev = new MatrixEvent({
|
||||||
@ -134,6 +117,21 @@ describe("MatrixEvent", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should report decryption errors", async () => {
|
||||||
|
const crypto = {
|
||||||
|
decryptEvent: jest.fn().mockRejectedValue(new Error("test error")),
|
||||||
|
} as unknown as Crypto;
|
||||||
|
|
||||||
|
await encryptedEvent.attemptDecryption(crypto);
|
||||||
|
expect(encryptedEvent.isEncrypted()).toBeTruthy();
|
||||||
|
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
|
||||||
|
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
|
||||||
|
expect(encryptedEvent.getContent()).toEqual({
|
||||||
|
msgtype: "m.bad.encrypted",
|
||||||
|
body: "** Unable to decrypt: Error: test error **",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should retry decryption if a retry is queued", async () => {
|
it("should retry decryption if a retry is queued", async () => {
|
||||||
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, "attemptDecryption");
|
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, "attemptDecryption");
|
||||||
|
|
||||||
|
@ -14,13 +14,21 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { M_POLL_START } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
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, RelationsEvent } from "../../src/models/relations";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
|
import { RelationType } from "../../src";
|
||||||
|
import { logger } from "../../src/logger";
|
||||||
|
|
||||||
describe("Relations", function () {
|
describe("Relations", function () {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.spyOn(logger, "error").mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
it("should deduplicate annotations", function () {
|
it("should deduplicate annotations", function () {
|
||||||
const room = new Room("room123", null!, null!);
|
const room = new Room("room123", null!, null!);
|
||||||
const relations = new Relations("m.annotation", "m.reaction", room);
|
const relations = new Relations("m.annotation", "m.reaction", room);
|
||||||
@ -75,6 +83,92 @@ describe("Relations", function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("addEvent()", () => {
|
||||||
|
const relationType = RelationType.Reference;
|
||||||
|
const eventType = M_POLL_START.stable!;
|
||||||
|
const altEventTypes = [M_POLL_START.unstable!];
|
||||||
|
const room = new Room("room123", null!, null!);
|
||||||
|
|
||||||
|
it("should not add events without a relation", async () => {
|
||||||
|
// dont pollute console
|
||||||
|
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||||
|
const relations = new Relations(relationType, eventType, room);
|
||||||
|
const emitSpy = jest.spyOn(relations, "emit");
|
||||||
|
const event = new MatrixEvent({ type: eventType });
|
||||||
|
|
||||||
|
await relations.addEvent(event);
|
||||||
|
expect(logSpy).toHaveBeenCalledWith("Event must have relation info");
|
||||||
|
// event not added
|
||||||
|
expect(relations.getRelations().length).toBe(0);
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not add events of incorrect event type", async () => {
|
||||||
|
// dont pollute console
|
||||||
|
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||||
|
const relations = new Relations(relationType, eventType, room);
|
||||||
|
const emitSpy = jest.spyOn(relations, "emit");
|
||||||
|
const event = new MatrixEvent({
|
||||||
|
type: "different-event-type",
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||||
|
rel_type: relationType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await relations.addEvent(event);
|
||||||
|
|
||||||
|
expect(logSpy).toHaveBeenCalledWith(`Event relation info doesn't match this container`);
|
||||||
|
// event not added
|
||||||
|
expect(relations.getRelations().length).toBe(0);
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds events that match alt event types", async () => {
|
||||||
|
const relations = new Relations(relationType, eventType, room, altEventTypes);
|
||||||
|
const emitSpy = jest.spyOn(relations, "emit");
|
||||||
|
const event = new MatrixEvent({
|
||||||
|
type: M_POLL_START.unstable!,
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||||
|
rel_type: relationType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await relations.addEvent(event);
|
||||||
|
|
||||||
|
// event added
|
||||||
|
expect(relations.getRelations()).toEqual([event]);
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(RelationsEvent.Add, event);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not add events of incorrect relation type", async () => {
|
||||||
|
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||||
|
const relations = new Relations(relationType, eventType, room);
|
||||||
|
const event = new MatrixEvent({
|
||||||
|
type: eventType,
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||||
|
rel_type: "m.annotation",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await relations.addEvent(event);
|
||||||
|
const emitSpy = jest.spyOn(relations, "emit");
|
||||||
|
|
||||||
|
expect(logSpy).toHaveBeenCalledWith(`Event relation info doesn't match this container`);
|
||||||
|
// event not added
|
||||||
|
expect(relations.getRelations().length).toBe(0);
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should emit created regardless of ordering", async function () {
|
it("should emit created regardless of ordering", async function () {
|
||||||
const targetEvent = new MatrixEvent({
|
const targetEvent = new MatrixEvent({
|
||||||
sender: "@bob:example.com",
|
sender: "@bob:example.com",
|
||||||
|
248
spec/unit/rust-crypto.spec.ts
Normal file
248
spec/unit/rust-crypto.spec.ts
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
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 "fake-indexeddb/auto";
|
||||||
|
import { IDBFactory } from "fake-indexeddb";
|
||||||
|
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||||
|
import {
|
||||||
|
KeysBackupRequest,
|
||||||
|
KeysClaimRequest,
|
||||||
|
KeysQueryRequest,
|
||||||
|
KeysUploadRequest,
|
||||||
|
OlmMachine,
|
||||||
|
SignatureUploadRequest,
|
||||||
|
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||||
|
import { Mocked } from "jest-mock";
|
||||||
|
import MockHttpBackend from "matrix-mock-request";
|
||||||
|
|
||||||
|
import { RustCrypto } from "../../src/rust-crypto/rust-crypto";
|
||||||
|
import { initRustCrypto } from "../../src/rust-crypto";
|
||||||
|
import { HttpApiEvent, HttpApiEventHandlerMap, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../src";
|
||||||
|
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||||
|
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||||
|
// eslint-disable-next-line no-global-assign
|
||||||
|
indexedDB = new IDBFactory();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("RustCrypto", () => {
|
||||||
|
const TEST_USER = "@alice:example.com";
|
||||||
|
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||||
|
|
||||||
|
describe(".exportRoomKeys", () => {
|
||||||
|
let rustCrypto: RustCrypto;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const mockHttpApi = {} as MatrixClient["http"];
|
||||||
|
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a list", async () => {
|
||||||
|
const keys = await rustCrypto.exportRoomKeys();
|
||||||
|
expect(Array.isArray(keys)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("to-device messages", () => {
|
||||||
|
let rustCrypto: RustCrypto;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const mockHttpApi = {} as MatrixClient["http"];
|
||||||
|
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass through unencrypted to-device messages", async () => {
|
||||||
|
const inputs: IToDeviceEvent[] = [
|
||||||
|
{ content: { key: "value" }, type: "org.matrix.test", sender: "@alice:example.com" },
|
||||||
|
];
|
||||||
|
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
|
||||||
|
expect(res).toEqual(inputs);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass through bad encrypted messages", async () => {
|
||||||
|
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
|
||||||
|
const keys = olmMachine.identityKeys;
|
||||||
|
const inputs: IToDeviceEvent[] = [
|
||||||
|
{
|
||||||
|
type: "m.room.encrypted",
|
||||||
|
content: {
|
||||||
|
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||||
|
sender_key: "IlRMeOPX2e0MurIyfWEucYBRVOEEUMrOHqn/8mLqMjA",
|
||||||
|
ciphertext: {
|
||||||
|
[keys.curve25519.toBase64()]: {
|
||||||
|
type: 0,
|
||||||
|
body: "ajyjlghi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
|
||||||
|
expect(res).toEqual(inputs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("outgoing requests", () => {
|
||||||
|
/** the RustCrypto implementation under test */
|
||||||
|
let rustCrypto: RustCrypto;
|
||||||
|
|
||||||
|
/** A mock http backend which rustCrypto is connected to */
|
||||||
|
let httpBackend: MockHttpBackend;
|
||||||
|
|
||||||
|
/** a mocked-up OlmMachine which rustCrypto is connected to */
|
||||||
|
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||||
|
|
||||||
|
/** A list of results to be returned from olmMachine.outgoingRequest. Each call will shift a result off
|
||||||
|
* the front of the queue, until it is empty. */
|
||||||
|
let outgoingRequestQueue: Array<Array<any>>;
|
||||||
|
|
||||||
|
/** wait for a call to olmMachine.markRequestAsSent */
|
||||||
|
function awaitCallToMarkAsSent(): Promise<void> {
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
|
||||||
|
resolve(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
httpBackend = new MockHttpBackend();
|
||||||
|
|
||||||
|
await RustSdkCryptoJs.initAsync();
|
||||||
|
|
||||||
|
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||||
|
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
|
||||||
|
baseUrl: "https://example.com",
|
||||||
|
prefix: "/_matrix",
|
||||||
|
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||||
|
onlyData: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// for these tests we use a mock OlmMachine, with an implementation of outgoingRequests that
|
||||||
|
// returns objects from outgoingRequestQueue
|
||||||
|
outgoingRequestQueue = [];
|
||||||
|
olmMachine = {
|
||||||
|
outgoingRequests: jest.fn().mockImplementation(() => {
|
||||||
|
return Promise.resolve(outgoingRequestQueue.shift() ?? []);
|
||||||
|
}),
|
||||||
|
markRequestAsSent: jest.fn(),
|
||||||
|
close: jest.fn(),
|
||||||
|
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||||
|
|
||||||
|
rustCrypto = new RustCrypto(olmMachine, httpApi, TEST_USER, TEST_DEVICE_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should poll for outgoing messages", () => {
|
||||||
|
rustCrypto.onSyncCompleted({});
|
||||||
|
expect(olmMachine.outgoingRequests).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* simple requests that map directly to the request body */
|
||||||
|
const tests: Array<[any, "POST" | "PUT", string]> = [
|
||||||
|
[KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"],
|
||||||
|
[KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"],
|
||||||
|
[KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"],
|
||||||
|
[SignatureUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/signatures/upload"],
|
||||||
|
[KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [RequestClass, expectedMethod, expectedPath] of tests) {
|
||||||
|
it(`should handle ${RequestClass.name}s`, async () => {
|
||||||
|
const testBody = '{ "foo": "bar" }';
|
||||||
|
const outgoingRequest = new RequestClass("1234", testBody);
|
||||||
|
outgoingRequestQueue.push([outgoingRequest]);
|
||||||
|
|
||||||
|
const testResponse = '{ "result": 1 }';
|
||||||
|
httpBackend
|
||||||
|
.when(expectedMethod, "/_matrix")
|
||||||
|
.check((req) => {
|
||||||
|
expect(req.path).toEqual(expectedPath);
|
||||||
|
expect(req.rawData).toEqual(testBody);
|
||||||
|
expect(req.headers["Accept"]).toEqual("application/json");
|
||||||
|
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||||
|
})
|
||||||
|
.respond(200, testResponse, true);
|
||||||
|
|
||||||
|
rustCrypto.onSyncCompleted({});
|
||||||
|
|
||||||
|
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
|
|
||||||
|
await markSentCallPromise;
|
||||||
|
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||||
|
httpBackend.verifyNoOutstandingRequests();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("does not explode with unknown requests", async () => {
|
||||||
|
const outgoingRequest = { id: "5678", type: 987 };
|
||||||
|
outgoingRequestQueue.push([outgoingRequest]);
|
||||||
|
|
||||||
|
rustCrypto.onSyncCompleted({});
|
||||||
|
|
||||||
|
await awaitCallToMarkAsSent();
|
||||||
|
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stops looping when stop() is called", async () => {
|
||||||
|
const testResponse = '{ "result": 1 }';
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
outgoingRequestQueue.push([new KeysQueryRequest("1234", "{}")]);
|
||||||
|
httpBackend.when("POST", "/_matrix").respond(200, testResponse, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
rustCrypto.onSyncCompleted({});
|
||||||
|
|
||||||
|
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeTruthy();
|
||||||
|
|
||||||
|
// go a couple of times round the loop
|
||||||
|
await httpBackend.flush("/_matrix", 1);
|
||||||
|
await awaitCallToMarkAsSent();
|
||||||
|
|
||||||
|
await httpBackend.flush("/_matrix", 1);
|
||||||
|
await awaitCallToMarkAsSent();
|
||||||
|
|
||||||
|
// a second sync while this is going on shouldn't make any difference
|
||||||
|
rustCrypto.onSyncCompleted({});
|
||||||
|
|
||||||
|
await httpBackend.flush("/_matrix", 1);
|
||||||
|
await awaitCallToMarkAsSent();
|
||||||
|
|
||||||
|
// now stop...
|
||||||
|
rustCrypto.stop();
|
||||||
|
|
||||||
|
// which should (eventually) cause the loop to stop with no further calls to outgoingRequests
|
||||||
|
olmMachine.outgoingRequests.mockReset();
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 100);
|
||||||
|
});
|
||||||
|
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeFalsy();
|
||||||
|
httpBackend.verifyNoOutstandingRequests();
|
||||||
|
expect(olmMachine.outgoingRequests).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// we sent three, so there should be 2 left
|
||||||
|
expect(outgoingRequestQueue.length).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -142,6 +142,15 @@ describe("Group Call", function () {
|
|||||||
} as unknown as RoomMember;
|
} as unknown as RoomMember;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(Object.values(GroupCallState).filter((v) => v !== GroupCallState.LocalCallFeedUninitialized))(
|
||||||
|
"throws when initializing local call feed in %s state",
|
||||||
|
async (state: GroupCallState) => {
|
||||||
|
// @ts-ignore
|
||||||
|
groupCall.state = state;
|
||||||
|
await expect(groupCall.initLocalCallFeed()).rejects.toThrowError();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("does not initialize local call feed, if it already is", async () => {
|
it("does not initialize local call feed, if it already is", async () => {
|
||||||
await groupCall.initLocalCallFeed();
|
await groupCall.initLocalCallFeed();
|
||||||
jest.spyOn(groupCall, "initLocalCallFeed");
|
jest.spyOn(groupCall, "initLocalCallFeed");
|
||||||
@ -308,6 +317,17 @@ describe("Group Call", function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("hasLocalParticipant()", () => {
|
||||||
|
it("should return false, if we don't have a local participant", () => {
|
||||||
|
expect(groupCall.hasLocalParticipant()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true, if we do have local participant", async () => {
|
||||||
|
await groupCall.enter();
|
||||||
|
expect(groupCall.hasLocalParticipant()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("call feeds changing", () => {
|
describe("call feeds changing", () => {
|
||||||
let call: MockMatrixCall;
|
let call: MockMatrixCall;
|
||||||
const currentFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("current"));
|
const currentFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("current"));
|
||||||
@ -475,7 +495,7 @@ describe("Group Call", function () {
|
|||||||
const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId);
|
const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
groupCall.calls.set(
|
groupCall.calls.set(
|
||||||
mockCall.getOpponentMember() as RoomMember,
|
mockCall.getOpponentMember().userId!,
|
||||||
new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]),
|
new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -501,7 +521,7 @@ describe("Group Call", function () {
|
|||||||
const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId);
|
const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
groupCall.calls.set(
|
groupCall.calls.set(
|
||||||
mockCall.getOpponentMember() as RoomMember,
|
mockCall.getOpponentMember().userId!,
|
||||||
new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]),
|
new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -663,9 +683,7 @@ describe("Group Call", function () {
|
|||||||
expect(client1.sendToDevice).toHaveBeenCalled();
|
expect(client1.sendToDevice).toHaveBeenCalled();
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const oldCall = groupCall1.calls
|
const oldCall = groupCall1.calls.get(client2.userId)!.get(client2.deviceId)!;
|
||||||
.get(groupCall1.room.getMember(client2.userId)!)!
|
|
||||||
.get(client2.deviceId)!;
|
|
||||||
oldCall.emit(CallEvent.Hangup, oldCall!);
|
oldCall.emit(CallEvent.Hangup, oldCall!);
|
||||||
|
|
||||||
client1.sendToDevice.mockClear();
|
client1.sendToDevice.mockClear();
|
||||||
@ -685,9 +703,7 @@ describe("Group Call", function () {
|
|||||||
let newCall: MatrixCall | undefined;
|
let newCall: MatrixCall | undefined;
|
||||||
while (
|
while (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(newCall = groupCall1.calls
|
(newCall = groupCall1.calls.get(client2.userId)?.get(client2.deviceId)) === undefined ||
|
||||||
.get(groupCall1.room.getMember(client2.userId)!)
|
|
||||||
?.get(client2.deviceId)) === undefined ||
|
|
||||||
newCall.peerConn === undefined ||
|
newCall.peerConn === undefined ||
|
||||||
newCall.callId == oldCall.callId
|
newCall.callId == oldCall.callId
|
||||||
) {
|
) {
|
||||||
@ -730,7 +746,7 @@ describe("Group Call", function () {
|
|||||||
groupCall1.setLocalVideoMuted(false);
|
groupCall1.setLocalVideoMuted(false);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const call = groupCall1.calls.get(groupCall1.room.getMember(client2.userId)!)!.get(client2.deviceId)!;
|
const call = groupCall1.calls.get(client2.userId)!.get(client2.deviceId)!;
|
||||||
call.isMicrophoneMuted = jest.fn().mockReturnValue(true);
|
call.isMicrophoneMuted = jest.fn().mockReturnValue(true);
|
||||||
call.setMicrophoneMuted = jest.fn();
|
call.setMicrophoneMuted = jest.fn();
|
||||||
call.isLocalVideoMuted = jest.fn().mockReturnValue(true);
|
call.isLocalVideoMuted = jest.fn().mockReturnValue(true);
|
||||||
@ -839,7 +855,7 @@ describe("Group Call", function () {
|
|||||||
await sleep(10);
|
await sleep(10);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!;
|
const call = groupCall.calls.get(FAKE_USER_ID_2)!.get(FAKE_DEVICE_ID_2)!;
|
||||||
call.getOpponentMember = () => ({ userId: call.invitee } as RoomMember);
|
call.getOpponentMember = () => ({ userId: call.invitee } as RoomMember);
|
||||||
// @ts-ignore Mock
|
// @ts-ignore Mock
|
||||||
call.pushRemoteFeed(
|
call.pushRemoteFeed(
|
||||||
@ -866,7 +882,7 @@ describe("Group Call", function () {
|
|||||||
await sleep(10);
|
await sleep(10);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!;
|
const call = groupCall.calls.get(FAKE_USER_ID_2).get(FAKE_DEVICE_ID_2)!;
|
||||||
call.getOpponentMember = () => ({ userId: call.invitee } as RoomMember);
|
call.getOpponentMember = () => ({ userId: call.invitee } as RoomMember);
|
||||||
// @ts-ignore Mock
|
// @ts-ignore Mock
|
||||||
call.pushRemoteFeed(
|
call.pushRemoteFeed(
|
||||||
@ -943,9 +959,7 @@ describe("Group Call", function () {
|
|||||||
expect(mockCall.reject).not.toHaveBeenCalled();
|
expect(mockCall.reject).not.toHaveBeenCalled();
|
||||||
expect(mockCall.answerWithCallFeeds).toHaveBeenCalled();
|
expect(mockCall.answerWithCallFeeds).toHaveBeenCalled();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(groupCall.calls).toEqual(
|
expect(groupCall.calls).toEqual(new Map([[FAKE_USER_ID_1, new Map([[FAKE_DEVICE_ID_1, mockCall]])]]));
|
||||||
new Map([[groupCall.room.getMember(FAKE_USER_ID_1)!, new Map([[FAKE_DEVICE_ID_1, mockCall]])]]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("replaces calls if it already has one with the same user", async () => {
|
it("replaces calls if it already has one with the same user", async () => {
|
||||||
@ -960,9 +974,7 @@ describe("Group Call", function () {
|
|||||||
expect(oldMockCall.hangup).toHaveBeenCalled();
|
expect(oldMockCall.hangup).toHaveBeenCalled();
|
||||||
expect(newMockCall.answerWithCallFeeds).toHaveBeenCalled();
|
expect(newMockCall.answerWithCallFeeds).toHaveBeenCalled();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(groupCall.calls).toEqual(
|
expect(groupCall.calls).toEqual(new Map([[FAKE_USER_ID_1, new Map([[FAKE_DEVICE_ID_1, newMockCall]])]]));
|
||||||
new Map([[groupCall.room.getMember(FAKE_USER_ID_1)!, new Map([[FAKE_DEVICE_ID_1, newMockCall]])]]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("starts to process incoming calls when we've entered", async () => {
|
it("starts to process incoming calls when we've entered", async () => {
|
||||||
@ -975,6 +987,83 @@ describe("Group Call", function () {
|
|||||||
|
|
||||||
expect(call.answerWithCallFeeds).toHaveBeenCalled();
|
expect(call.answerWithCallFeeds).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("handles call being replaced", () => {
|
||||||
|
let callChangedListener: jest.Mock;
|
||||||
|
let oldMockCall: MockMatrixCall;
|
||||||
|
let newMockCall: MockMatrixCall;
|
||||||
|
let newCallsMap: Map<string, Map<string, MatrixCall>>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
callChangedListener = jest.fn();
|
||||||
|
groupCall.addListener(GroupCallEvent.CallsChanged, callChangedListener);
|
||||||
|
|
||||||
|
oldMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||||
|
newMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||||
|
newCallsMap = new Map([[FAKE_USER_ID_1, new Map([[FAKE_DEVICE_ID_1, newMockCall.typed()]])]]);
|
||||||
|
|
||||||
|
newMockCall.opponentMember = oldMockCall.opponentMember; // Ensure referential equality
|
||||||
|
newMockCall.callId = "not " + oldMockCall.callId;
|
||||||
|
mockClient.emit(CallEventHandlerEvent.Incoming, oldMockCall.typed());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles regular case", () => {
|
||||||
|
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed());
|
||||||
|
|
||||||
|
expect(oldMockCall.hangup).toHaveBeenCalled();
|
||||||
|
expect(callChangedListener).toHaveBeenCalledWith(newCallsMap);
|
||||||
|
// @ts-ignore
|
||||||
|
expect(groupCall.calls).toEqual(newCallsMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles case where call is missing from the calls map", () => {
|
||||||
|
// @ts-ignore
|
||||||
|
groupCall.calls = new Map();
|
||||||
|
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed());
|
||||||
|
|
||||||
|
expect(oldMockCall.hangup).toHaveBeenCalled();
|
||||||
|
expect(callChangedListener).toHaveBeenCalledWith(newCallsMap);
|
||||||
|
// @ts-ignore
|
||||||
|
expect(groupCall.calls).toEqual(newCallsMap);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("handles call being hangup", () => {
|
||||||
|
let callChangedListener: jest.Mock;
|
||||||
|
let mockCall: MockMatrixCall;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
callChangedListener = jest.fn();
|
||||||
|
groupCall.addListener(GroupCallEvent.CallsChanged, callChangedListener);
|
||||||
|
mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't throw when calls map is empty", () => {
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => groupCall.onCallHangup(mockCall)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears map completely when we're the last users device left", () => {
|
||||||
|
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall.typed());
|
||||||
|
mockCall.emit(CallEvent.Hangup, mockCall.typed());
|
||||||
|
// @ts-ignore
|
||||||
|
expect(groupCall.calls).toEqual(new Map());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't remove another call of the same user", () => {
|
||||||
|
const anotherCallOfTheSameUser = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||||
|
anotherCallOfTheSameUser.callId = "another call id";
|
||||||
|
anotherCallOfTheSameUser.getOpponentDeviceId = () => FAKE_DEVICE_ID_2;
|
||||||
|
mockClient.emit(CallEventHandlerEvent.Incoming, anotherCallOfTheSameUser.typed());
|
||||||
|
|
||||||
|
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall.typed());
|
||||||
|
mockCall.emit(CallEvent.Hangup, mockCall.typed());
|
||||||
|
// @ts-ignore
|
||||||
|
expect(groupCall.calls).toEqual(
|
||||||
|
new Map([[FAKE_USER_ID_1, new Map([[FAKE_DEVICE_ID_2, anotherCallOfTheSameUser.typed()]])]]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("screensharing", () => {
|
describe("screensharing", () => {
|
||||||
@ -1039,7 +1128,7 @@ describe("Group Call", function () {
|
|||||||
await sleep(10);
|
await sleep(10);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!;
|
const call = groupCall.calls.get(FAKE_USER_ID_2)!.get(FAKE_DEVICE_ID_2)!;
|
||||||
call.getOpponentMember = () => ({ userId: call.invitee } as RoomMember);
|
call.getOpponentMember = () => ({ userId: call.invitee } as RoomMember);
|
||||||
call.onNegotiateReceived({
|
call.onNegotiateReceived({
|
||||||
getContent: () => ({
|
getContent: () => ({
|
||||||
|
@ -44,3 +44,29 @@ export interface IEventDecryptionResult {
|
|||||||
claimedEd25519Key?: string;
|
claimedEd25519Key?: string;
|
||||||
untrusted?: boolean;
|
untrusted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Extensible {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
/** The result of a call to {@link MatrixClient.exportRoomKeys} */
|
||||||
|
export interface IMegolmSessionData extends Extensible {
|
||||||
|
/** Sender's Curve25519 device key */
|
||||||
|
sender_key: string;
|
||||||
|
/** Devices which forwarded this session to us (normally empty). */
|
||||||
|
forwarding_curve25519_key_chain: string[];
|
||||||
|
/** Other keys the sender claims. */
|
||||||
|
sender_claimed_keys: Record<string, string>;
|
||||||
|
/** Room this session is used in */
|
||||||
|
room_id: string;
|
||||||
|
/** Unique id for the session */
|
||||||
|
session_id: string;
|
||||||
|
/** Base64'ed key data */
|
||||||
|
session_key: string;
|
||||||
|
algorithm?: string;
|
||||||
|
untrusted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
@ -61,11 +61,12 @@ export enum EventType {
|
|||||||
KeyVerificationDone = "m.key.verification.done",
|
KeyVerificationDone = "m.key.verification.done",
|
||||||
KeyVerificationKey = "m.key.verification.key",
|
KeyVerificationKey = "m.key.verification.key",
|
||||||
KeyVerificationAccept = "m.key.verification.accept",
|
KeyVerificationAccept = "m.key.verification.accept",
|
||||||
// XXX this event is not yet supported by js-sdk
|
// Not used directly - see READY_TYPE in VerificationRequest.
|
||||||
KeyVerificationReady = "m.key.verification.ready",
|
KeyVerificationReady = "m.key.verification.ready",
|
||||||
// use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
|
// use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
|
||||||
RoomMessageFeedback = "m.room.message.feedback",
|
RoomMessageFeedback = "m.room.message.feedback",
|
||||||
Reaction = "m.reaction",
|
Reaction = "m.reaction",
|
||||||
|
PollStart = "org.matrix.msc3381.poll.start",
|
||||||
|
|
||||||
// Room ephemeral events
|
// Room ephemeral events
|
||||||
Typing = "m.typing",
|
Typing = "m.typing",
|
||||||
@ -165,6 +166,15 @@ export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix
|
|||||||
*/
|
*/
|
||||||
export const UNSTABLE_MSC2716_MARKER = new UnstableValue("m.room.marker", "org.matrix.msc2716.marker");
|
export const UNSTABLE_MSC2716_MARKER = new UnstableValue("m.room.marker", "org.matrix.msc2716.marker");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the "with_relations" request property for relation based redactions.
|
||||||
|
* {@link https://github.com/matrix-org/matrix-spec-proposals/pull/3912}
|
||||||
|
*/
|
||||||
|
export const MSC3912_RELATION_BASED_REDACTIONS_PROP = new UnstableValue(
|
||||||
|
"with_relations",
|
||||||
|
"org.matrix.msc3912.with_relations",
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functional members type for declaring a purpose of room members (e.g. helpful bots).
|
* Functional members type for declaring a purpose of room members (e.g. helpful bots).
|
||||||
* Note that this reference is UNSTABLE and subject to breaking changes, including its
|
* Note that this reference is UNSTABLE and subject to breaking changes, including its
|
||||||
|
@ -54,3 +54,11 @@ export type Receipts = {
|
|||||||
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
|
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CachedReceiptStructure = {
|
||||||
|
eventId: string;
|
||||||
|
receiptType: string | ReceiptType;
|
||||||
|
userId: string;
|
||||||
|
receipt: Receipt;
|
||||||
|
synthetic: boolean;
|
||||||
|
};
|
||||||
|
@ -21,7 +21,7 @@ import { IRoomEventFilter } from "../filter";
|
|||||||
import { Direction } from "../models/event-timeline";
|
import { Direction } from "../models/event-timeline";
|
||||||
import { PushRuleAction } from "./PushRules";
|
import { PushRuleAction } from "./PushRules";
|
||||||
import { IRoomEvent } from "../sync-accumulator";
|
import { IRoomEvent } from "../sync-accumulator";
|
||||||
import { EventType, RoomType } from "./event";
|
import { EventType, RelationType, RoomType } from "./event";
|
||||||
|
|
||||||
// allow camelcase as these are things that go onto the wire
|
// allow camelcase as these are things that go onto the wire
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
@ -47,6 +47,18 @@ export interface IJoinRoomOpts {
|
|||||||
|
|
||||||
export interface IRedactOpts {
|
export interface IRedactOpts {
|
||||||
reason?: string;
|
reason?: string;
|
||||||
|
/**
|
||||||
|
* Whether events related to the redacted event should be redacted.
|
||||||
|
*
|
||||||
|
* If specified, then any events which relate to the event being redacted with
|
||||||
|
* any of the relationship types listed will also be redacted.
|
||||||
|
*
|
||||||
|
* <b>Raises an Error if the server does not support it.</b>
|
||||||
|
* Check for server-side support before using this param with
|
||||||
|
* <code>client.canSupport.get(Feature.RelationBasedRedactions)</code>.
|
||||||
|
* {@link https://github.com/matrix-org/matrix-spec-proposals/pull/3912}
|
||||||
|
*/
|
||||||
|
with_relations?: Array<RelationType | string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISendEventResponse {
|
export interface ISendEventResponse {
|
||||||
|
@ -214,9 +214,9 @@ export class AutoDiscovery {
|
|||||||
|
|
||||||
// Step 5b: Verify there is an identity server listening on the provided
|
// Step 5b: Verify there is an identity server listening on the provided
|
||||||
// URL.
|
// URL.
|
||||||
const isResponse = await this.fetchWellKnownObject(`${isUrl}/_matrix/identity/api/v1`);
|
const isResponse = await this.fetchWellKnownObject(`${isUrl}/_matrix/identity/v2`);
|
||||||
if (!isResponse?.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
|
if (!isResponse?.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
|
||||||
logger.error("Invalid /api/v1 response");
|
logger.error("Invalid /v2 response");
|
||||||
failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
|
failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
|
||||||
|
|
||||||
// Supply the base_url to the caller because they may be ignoring
|
// Supply the base_url to the caller because they may be ignoring
|
||||||
|
105
src/client.ts
105
src/client.ts
@ -20,7 +20,8 @@ limitations under the License.
|
|||||||
|
|
||||||
import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk";
|
import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { ISyncStateData, SyncApi, SyncState } from "./sync";
|
import type { IMegolmSessionData } from "./@types/crypto";
|
||||||
|
import { ISyncStateData, SyncApi, SyncApiOptions, SyncState } from "./sync";
|
||||||
import {
|
import {
|
||||||
EventStatus,
|
EventStatus,
|
||||||
IContent,
|
IContent,
|
||||||
@ -74,7 +75,6 @@ import {
|
|||||||
ICryptoCallbacks,
|
ICryptoCallbacks,
|
||||||
IBootstrapCrossSigningOpts,
|
IBootstrapCrossSigningOpts,
|
||||||
ICheckOwnCrossSigningTrustOpts,
|
ICheckOwnCrossSigningTrustOpts,
|
||||||
IMegolmSessionData,
|
|
||||||
isCryptoAvailable,
|
isCryptoAvailable,
|
||||||
VerificationMethod,
|
VerificationMethod,
|
||||||
IRoomKeyRequestBody,
|
IRoomKeyRequestBody,
|
||||||
@ -154,6 +154,7 @@ import {
|
|||||||
UNSTABLE_MSC3088_ENABLED,
|
UNSTABLE_MSC3088_ENABLED,
|
||||||
UNSTABLE_MSC3088_PURPOSE,
|
UNSTABLE_MSC3088_PURPOSE,
|
||||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||||
|
MSC3912_RELATION_BASED_REDACTIONS_PROP,
|
||||||
} from "./@types/event";
|
} from "./@types/event";
|
||||||
import { IdServerUnbindResult, IImageInfo, Preset, Visibility } from "./@types/partials";
|
import { IdServerUnbindResult, IImageInfo, Preset, Visibility } from "./@types/partials";
|
||||||
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper";
|
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper";
|
||||||
@ -455,17 +456,7 @@ export interface IStartClientOpts {
|
|||||||
slidingSync?: SlidingSync;
|
slidingSync?: SlidingSync;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStoredClientOpts extends IStartClientOpts {
|
export interface IStoredClientOpts extends IStartClientOpts {}
|
||||||
// Crypto manager
|
|
||||||
crypto?: Crypto;
|
|
||||||
/**
|
|
||||||
* A function which is called
|
|
||||||
* with a room ID and returns a boolean. It should return 'true' if the SDK can
|
|
||||||
* SAFELY remove events from this room. It may not be safe to remove events if
|
|
||||||
* there are other references to the timelines for this room.
|
|
||||||
*/
|
|
||||||
canResetEntireTimeline: ResetTimelineCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum RoomVersionStability {
|
export enum RoomVersionStability {
|
||||||
Stable = "stable",
|
Stable = "stable",
|
||||||
@ -841,6 +832,11 @@ interface ITimestampToEventResponse {
|
|||||||
event_id: string;
|
event_id: string;
|
||||||
origin_server_ts: string;
|
origin_server_ts: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IWhoamiResponse {
|
||||||
|
user_id: string;
|
||||||
|
device_id?: string;
|
||||||
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
// We're using this constant for methods overloading and inspect whether a variable
|
// We're using this constant for methods overloading and inspect whether a variable
|
||||||
@ -1427,20 +1423,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
logger.error("Can't fetch server versions, continuing to initialise sync, this will be retried later", e);
|
logger.error("Can't fetch server versions, continuing to initialise sync, this will be retried later", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// shallow-copy the opts dict before modifying and storing it
|
this.clientOpts = opts ?? {};
|
||||||
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
|
|
||||||
this.clientOpts.crypto = this.crypto;
|
|
||||||
this.clientOpts.canResetEntireTimeline = (roomId): boolean => {
|
|
||||||
if (!this.canResetTimelineCallback) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.canResetTimelineCallback(roomId);
|
|
||||||
};
|
|
||||||
if (this.clientOpts.slidingSync) {
|
if (this.clientOpts.slidingSync) {
|
||||||
this.syncApi = new SlidingSyncSdk(this.clientOpts.slidingSync, this, this.clientOpts);
|
this.syncApi = new SlidingSyncSdk(
|
||||||
|
this.clientOpts.slidingSync,
|
||||||
|
this,
|
||||||
|
this.clientOpts,
|
||||||
|
this.buildSyncApiOptions(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.syncApi = new SyncApi(this, this.clientOpts);
|
this.syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.syncApi.sync();
|
this.syncApi.sync();
|
||||||
|
|
||||||
if (this.clientOpts.clientWellKnownPollPeriod !== undefined) {
|
if (this.clientOpts.clientWellKnownPollPeriod !== undefined) {
|
||||||
@ -1453,6 +1447,22 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
this.toDeviceMessageQueue.start();
|
this.toDeviceMessageQueue.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a SyncApiOptions for this client, suitable for passing into the SyncApi constructor
|
||||||
|
*/
|
||||||
|
protected buildSyncApiOptions(): SyncApiOptions {
|
||||||
|
return {
|
||||||
|
crypto: this.crypto,
|
||||||
|
cryptoCallbacks: this.cryptoBackend,
|
||||||
|
canResetEntireTimeline: (roomId: string): boolean => {
|
||||||
|
if (!this.canResetTimelineCallback) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.canResetTimelineCallback(roomId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* High level helper method to stop the client from polling and allow a
|
* High level helper method to stop the client from polling and allow a
|
||||||
* clean shutdown.
|
* clean shutdown.
|
||||||
@ -2138,7 +2148,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
// importing rust-crypto will download the webassembly, so we delay it until we know it will be
|
// importing rust-crypto will download the webassembly, so we delay it until we know it will be
|
||||||
// needed.
|
// needed.
|
||||||
const RustCrypto = await import("./rust-crypto");
|
const RustCrypto = await import("./rust-crypto");
|
||||||
this.cryptoBackend = await RustCrypto.initRustCrypto(userId, deviceId);
|
this.cryptoBackend = await RustCrypto.initRustCrypto(this.http, userId, deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3033,10 +3043,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* session export objects
|
* session export objects
|
||||||
*/
|
*/
|
||||||
public exportRoomKeys(): Promise<IMegolmSessionData[]> {
|
public exportRoomKeys(): Promise<IMegolmSessionData[]> {
|
||||||
if (!this.crypto) {
|
if (!this.cryptoBackend) {
|
||||||
return Promise.reject(new Error("End-to-end encryption disabled"));
|
return Promise.reject(new Error("End-to-end encryption disabled"));
|
||||||
}
|
}
|
||||||
return this.crypto.exportRoomKeys();
|
return this.cryptoBackend.exportRoomKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3948,7 +3958,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
const res = await this.http.authedRequest<{ room_id: string }>(Method.Post, path, queryString, data);
|
const res = await this.http.authedRequest<{ room_id: string }>(Method.Post, path, queryString, data);
|
||||||
|
|
||||||
const roomId = res.room_id;
|
const roomId = res.room_id;
|
||||||
const syncApi = new SyncApi(this, this.clientOpts);
|
const syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
|
||||||
const room = syncApi.createRoom(roomId);
|
const room = syncApi.createRoom(roomId);
|
||||||
if (opts.syncRoom) {
|
if (opts.syncRoom) {
|
||||||
// v2 will do this for us
|
// v2 will do this for us
|
||||||
@ -4444,9 +4454,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param txnId - transaction id. One will be made up if not supplied.
|
* @param txnId - transaction id. One will be made up if not supplied.
|
||||||
* @param opts - Options to pass on, may contain `reason`.
|
* @param opts - Options to pass on, may contain `reason` and `with_relations` (MSC3912)
|
||||||
* @returns Promise which resolves: TODO
|
* @returns Promise which resolves: TODO
|
||||||
* @returns Rejects: with an error response.
|
* @returns Rejects: with an error response.
|
||||||
|
* @throws Error if called with `with_relations` (MSC3912) but the server does not support it.
|
||||||
|
* Callers should check whether the server supports MSC3912 via `MatrixClient.canSupport`.
|
||||||
*/
|
*/
|
||||||
public redactEvent(
|
public redactEvent(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
@ -4475,12 +4487,34 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
threadId = null;
|
threadId = null;
|
||||||
}
|
}
|
||||||
const reason = opts?.reason;
|
const reason = opts?.reason;
|
||||||
|
|
||||||
|
if (
|
||||||
|
opts?.with_relations &&
|
||||||
|
this.canSupport.get(Feature.RelationBasedRedactions) === ServerSupport.Unsupported
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Server does not support relation based redactions " +
|
||||||
|
`roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId ${threadId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const withRelations = opts?.with_relations
|
||||||
|
? {
|
||||||
|
[this.canSupport.get(Feature.RelationBasedRedactions) === ServerSupport.Stable
|
||||||
|
? MSC3912_RELATION_BASED_REDACTIONS_PROP.stable!
|
||||||
|
: MSC3912_RELATION_BASED_REDACTIONS_PROP.unstable!]: opts?.with_relations,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
return this.sendCompleteEvent(
|
return this.sendCompleteEvent(
|
||||||
roomId,
|
roomId,
|
||||||
threadId,
|
threadId,
|
||||||
{
|
{
|
||||||
type: EventType.RoomRedaction,
|
type: EventType.RoomRedaction,
|
||||||
content: { reason },
|
content: {
|
||||||
|
...withRelations,
|
||||||
|
reason,
|
||||||
|
},
|
||||||
redacts: eventId,
|
redacts: eventId,
|
||||||
},
|
},
|
||||||
txnId as string,
|
txnId as string,
|
||||||
@ -6059,7 +6093,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
this.processBeaconEvents(room, timelineEvents);
|
this.processBeaconEvents(room, timelineEvents);
|
||||||
this.processThreadRoots(
|
this.processThreadRoots(
|
||||||
room,
|
room,
|
||||||
timelineEvents.filter((it) => it.isRelation(THREAD_RELATION_TYPE.name)),
|
timelineEvents.filter((it) => it.getServerAggregatedRelation(THREAD_RELATION_TYPE.name)),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -6124,7 +6158,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
*/
|
*/
|
||||||
public peekInRoom(roomId: string): Promise<Room> {
|
public peekInRoom(roomId: string): Promise<Room> {
|
||||||
this.peekSync?.stopPeeking();
|
this.peekSync?.stopPeeking();
|
||||||
this.peekSync = new SyncApi(this, this.clientOpts);
|
this.peekSync = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
|
||||||
return this.peekSync.peek(roomId);
|
return this.peekSync.peek(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6631,7 +6665,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
if (this.syncLeftRoomsPromise) {
|
if (this.syncLeftRoomsPromise) {
|
||||||
return this.syncLeftRoomsPromise; // return the ongoing request
|
return this.syncLeftRoomsPromise; // return the ongoing request
|
||||||
}
|
}
|
||||||
const syncApi = new SyncApi(this, this.clientOpts);
|
const syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
|
||||||
this.syncLeftRoomsPromise = syncApi.syncLeftRooms();
|
this.syncLeftRoomsPromise = syncApi.syncLeftRooms();
|
||||||
|
|
||||||
// cleanup locks
|
// cleanup locks
|
||||||
@ -9350,10 +9384,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the user_id of the configured access token.
|
* Fetches information about the user for the configured access token.
|
||||||
*/
|
*/
|
||||||
public async whoami(): Promise<{ user_id: string }> {
|
public async whoami(): Promise<IWhoamiResponse> {
|
||||||
// eslint-disable-line camelcase
|
|
||||||
return this.http.authedRequest(Method.Get, "/account/whoami");
|
return this.http.authedRequest(Method.Get, "/account/whoami");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IEventDecryptionResult } from "../@types/crypto";
|
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||||
|
import type { IToDeviceEvent } from "../sync-accumulator";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common interface for the crypto implementations
|
* Common interface for the crypto implementations
|
||||||
*/
|
*/
|
||||||
export interface CryptoBackend {
|
export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||||
/**
|
/**
|
||||||
* Global override for whether the client should ever send encrypted
|
* Global override for whether the client should ever send encrypted
|
||||||
* messages to unverified devices. This provides the default for rooms which
|
* messages to unverified devices. This provides the default for rooms which
|
||||||
@ -60,4 +61,52 @@ export interface CryptoBackend {
|
|||||||
* Rejects with an error if there is a problem decrypting the event.
|
* Rejects with an error if there is a problem decrypting the event.
|
||||||
*/
|
*/
|
||||||
decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult>;
|
decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list containing all of the room keys
|
||||||
|
*
|
||||||
|
* This should be encrypted before returning it to the user.
|
||||||
|
*
|
||||||
|
* @returns a promise which resolves to a list of
|
||||||
|
* session export objects
|
||||||
|
*/
|
||||||
|
exportRoomKeys(): Promise<IMegolmSessionData[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The methods which crypto implementations should expose to the Sync api */
|
||||||
|
export interface SyncCryptoCallbacks {
|
||||||
|
/**
|
||||||
|
* Called by the /sync loop whenever there are incoming to-device messages.
|
||||||
|
*
|
||||||
|
* The implementation may preprocess the received messages (eg, decrypt them) and return an
|
||||||
|
* updated list of messages for dispatch to the rest of the system.
|
||||||
|
*
|
||||||
|
* Note that, unlike {@link ClientEvent.ToDeviceEvent} events, this is called on the raw to-device
|
||||||
|
* messages, rather than the results of any decryption attempts.
|
||||||
|
*
|
||||||
|
* @param events - the received to-device messages
|
||||||
|
* @returns A list of preprocessed to-device messages.
|
||||||
|
*/
|
||||||
|
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the /sync loop after each /sync response is processed.
|
||||||
|
*
|
||||||
|
* Used to complete batch processing, or to initiate background processes
|
||||||
|
*
|
||||||
|
* @param syncState - information about the completed sync.
|
||||||
|
*/
|
||||||
|
onSyncCompleted(syncState: OnSyncCompletedData): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnSyncCompletedData {
|
||||||
|
/**
|
||||||
|
* The 'next_batch' result from /sync, which will become the 'since' token for the next call to /sync.
|
||||||
|
*/
|
||||||
|
nextSyncToken?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if we are working our way through a backlog of events after connecting.
|
||||||
|
*/
|
||||||
|
catchingUp?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,7 @@ 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";
|
||||||
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
|
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
|
||||||
import { IMegolmSessionData } from "./index";
|
import { IMegolmSessionData, OlmGroupSessionExtraData } from "../@types/crypto";
|
||||||
import { OlmGroupSessionExtraData } from "../@types/crypto";
|
|
||||||
import { IMessage } from "./algorithms/olm";
|
import { IMessage } from "./algorithms/olm";
|
||||||
|
|
||||||
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
||||||
|
@ -18,11 +18,12 @@ limitations under the License.
|
|||||||
* Internal module. Defines the base classes of the encryption implementations
|
* Internal module. Defines the base classes of the encryption implementations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { IMegolmSessionData } from "../../@types/crypto";
|
||||||
import { MatrixClient } from "../../client";
|
import { MatrixClient } from "../../client";
|
||||||
import { Room } from "../../models/room";
|
import { Room } from "../../models/room";
|
||||||
import { OlmDevice } from "../OlmDevice";
|
import { OlmDevice } from "../OlmDevice";
|
||||||
import { IContent, MatrixEvent, RoomMember } from "../../matrix";
|
import { IContent, MatrixEvent, RoomMember } from "../../matrix";
|
||||||
import { Crypto, IEncryptedContent, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
|
import { Crypto, IEncryptedContent, IEventDecryptionResult, IncomingRoomKeyRequest } from "..";
|
||||||
import { DeviceInfo } from "../deviceinfo";
|
import { DeviceInfo } from "../deviceinfo";
|
||||||
import { IRoomEncryption } from "../RoomList";
|
import { IRoomEncryption } from "../RoomList";
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
import type { IEventDecryptionResult } from "../../@types/crypto";
|
import type { IEventDecryptionResult, IMegolmSessionData } from "../../@types/crypto";
|
||||||
import { logger } from "../../logger";
|
import { logger } from "../../logger";
|
||||||
import * as olmlib from "../olmlib";
|
import * as olmlib from "../olmlib";
|
||||||
import {
|
import {
|
||||||
@ -39,7 +39,7 @@ import { IOlmSessionResult } from "../olmlib";
|
|||||||
import { DeviceInfoMap } from "../DeviceList";
|
import { DeviceInfoMap } from "../DeviceList";
|
||||||
import { IContent, MatrixEvent } from "../../models/event";
|
import { IContent, MatrixEvent } from "../../models/event";
|
||||||
import { EventType, MsgType, ToDeviceMessageId } from "../../@types/event";
|
import { EventType, MsgType, ToDeviceMessageId } from "../../@types/event";
|
||||||
import { IMegolmEncryptedContent, IMegolmSessionData, IncomingRoomKeyRequest, IEncryptedContent } from "../index";
|
import { IMegolmEncryptedContent, IncomingRoomKeyRequest, IEncryptedContent } from "../index";
|
||||||
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
|
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
|
||||||
import { OlmGroupSessionExtraData } from "../../@types/crypto";
|
import { OlmGroupSessionExtraData } from "../../@types/crypto";
|
||||||
import { MatrixError } from "../../http-api";
|
import { MatrixError } from "../../http-api";
|
||||||
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||||||
* Classes for dealing with key backup.
|
* Classes for dealing with key backup.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { IMegolmSessionData } from "../@types/crypto";
|
||||||
import { MatrixClient } from "../client";
|
import { MatrixClient } from "../client";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
|
import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
|
||||||
@ -36,7 +37,7 @@ import {
|
|||||||
IKeyBackupSession,
|
IKeyBackupSession,
|
||||||
} from "./keybackup";
|
} from "./keybackup";
|
||||||
import { UnstableValue } from "../NamespacedValue";
|
import { UnstableValue } from "../NamespacedValue";
|
||||||
import { CryptoEvent, IMegolmSessionData } from "./index";
|
import { CryptoEvent } from "./index";
|
||||||
import { crypto } from "./crypto";
|
import { crypto } from "./crypto";
|
||||||
import { HTTPError, MatrixError } from "../http-api";
|
import { HTTPError, MatrixError } from "../http-api";
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ limitations under the License.
|
|||||||
import anotherjson from "another-json";
|
import anotherjson from "another-json";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
import type { IEventDecryptionResult } from "../@types/crypto";
|
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||||
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
|
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
|
||||||
import { EventType, ToDeviceMessageId } from "../@types/event";
|
import { EventType, ToDeviceMessageId } from "../@types/event";
|
||||||
import { TypedReEmitter } from "../ReEmitter";
|
import { TypedReEmitter } from "../ReEmitter";
|
||||||
@ -85,10 +85,11 @@ import { CryptoStore } from "./store/base";
|
|||||||
import { IVerificationChannel } from "./verification/request/Channel";
|
import { IVerificationChannel } from "./verification/request/Channel";
|
||||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||||
import { IContent } from "../models/event";
|
import { IContent } from "../models/event";
|
||||||
import { ISyncResponse } from "../sync-accumulator";
|
import { ISyncResponse, IToDeviceEvent } from "../sync-accumulator";
|
||||||
import { ISignatures } from "../@types/signed";
|
import { ISignatures } from "../@types/signed";
|
||||||
import { IMessage } from "./algorithms/olm";
|
import { IMessage } from "./algorithms/olm";
|
||||||
import { CryptoBackend } from "../common-crypto/CryptoBackend";
|
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||||
|
import { RoomState, RoomStateEvent } from "../models/room-state";
|
||||||
|
|
||||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||||
|
|
||||||
@ -171,26 +172,6 @@ export interface IRoomKeyRequestBody extends IRoomKey {
|
|||||||
sender_key: string;
|
sender_key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Extensible {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMegolmSessionData extends Extensible {
|
|
||||||
// Sender's Curve25519 device key
|
|
||||||
sender_key: string;
|
|
||||||
// Devices which forwarded this session to us (normally empty).
|
|
||||||
forwarding_curve25519_key_chain: string[];
|
|
||||||
// Other keys the sender claims.
|
|
||||||
sender_claimed_keys: Record<string, string>;
|
|
||||||
// Room this session is used in
|
|
||||||
room_id: string;
|
|
||||||
// Unique id for the session
|
|
||||||
session_id: string;
|
|
||||||
// Base64'ed key data
|
|
||||||
session_key: string;
|
|
||||||
algorithm?: string;
|
|
||||||
untrusted?: boolean;
|
|
||||||
}
|
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
interface IDeviceVerificationUpgrade {
|
interface IDeviceVerificationUpgrade {
|
||||||
@ -2244,9 +2225,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
await upload({ shouldEmit: true });
|
await upload({ shouldEmit: true });
|
||||||
// XXX: we'll need to wait for the device list to be updated
|
// XXX: we'll need to wait for the device list to be updated
|
||||||
}
|
}
|
||||||
|
|
||||||
// redo key requests after verification
|
|
||||||
this.cancelAndResendAllOutgoingKeyRequests();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceObj = DeviceInfo.fromStorage(dev, deviceId);
|
const deviceObj = DeviceInfo.fromStorage(dev, deviceId);
|
||||||
@ -2626,14 +2604,23 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
await storeConfigPromise;
|
await storeConfigPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.lazyLoadMembers) {
|
logger.log(`Enabling encryption in ${roomId}`);
|
||||||
logger.log(
|
|
||||||
"Enabling encryption in " + roomId + "; " + "starting to track device lists for all users therein",
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// we don't want to force a download of the full membership list of this room, but as soon as we have that
|
||||||
|
// list we can start tracking the device list.
|
||||||
|
if (room.membersLoaded()) {
|
||||||
await this.trackRoomDevicesImpl(room);
|
await this.trackRoomDevicesImpl(room);
|
||||||
} else {
|
} else {
|
||||||
logger.log("Enabling encryption in " + roomId);
|
// wait for the membership list to be loaded
|
||||||
|
const onState = (_state: RoomState): void => {
|
||||||
|
room.off(RoomStateEvent.Update, onState);
|
||||||
|
if (room.membersLoaded()) {
|
||||||
|
this.trackRoomDevicesImpl(room).catch((e) => {
|
||||||
|
logger.error(`Error enabling device tracking in ${roomId}`, e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
room.on(RoomStateEvent.Update, onState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2642,6 +2629,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
*
|
*
|
||||||
* @param roomId - The room ID to start tracking devices in.
|
* @param roomId - The room ID to start tracking devices in.
|
||||||
* @returns when all devices for the room have been fetched and marked to track
|
* @returns when all devices for the room have been fetched and marked to track
|
||||||
|
* @deprecated there's normally no need to call this function: device list tracking
|
||||||
|
* will be enabled as soon as we have the full membership list.
|
||||||
*/
|
*/
|
||||||
public trackRoomDevices(roomId: string): Promise<void> {
|
public trackRoomDevices(roomId: string): Promise<void> {
|
||||||
const room = this.clientStore.getRoom(roomId);
|
const room = this.clientStore.getRoom(roomId);
|
||||||
@ -2886,11 +2875,22 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
*/
|
*/
|
||||||
public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
|
public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
|
||||||
if (event.isRedacted()) {
|
if (event.isRedacted()) {
|
||||||
|
// Try to decrypt the redaction event, to support encrypted
|
||||||
|
// redaction reasons. If we can't decrypt, just fall back to using
|
||||||
|
// the original redacted_because.
|
||||||
const redactionEvent = new MatrixEvent({
|
const redactionEvent = new MatrixEvent({
|
||||||
room_id: event.getRoomId(),
|
room_id: event.getRoomId(),
|
||||||
...event.getUnsigned().redacted_because,
|
...event.getUnsigned().redacted_because,
|
||||||
});
|
});
|
||||||
const decryptedEvent = await this.decryptEvent(redactionEvent);
|
let redactedBecause: IEvent = event.getUnsigned().redacted_because!;
|
||||||
|
if (redactionEvent.isEncrypted()) {
|
||||||
|
try {
|
||||||
|
const decryptedEvent = await this.decryptEvent(redactionEvent);
|
||||||
|
redactedBecause = decryptedEvent.clearEvent as IEvent;
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("Decryption of redaction failed. Falling back to unencrypted event.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
clearEvent: {
|
clearEvent: {
|
||||||
@ -2898,7 +2898,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
content: {},
|
content: {},
|
||||||
unsigned: {
|
unsigned: {
|
||||||
redacted_because: decryptedEvent.clearEvent as IEvent,
|
redacted_because: redactedBecause,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -3021,7 +3021,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
*
|
*
|
||||||
* @param syncData - the data from the 'MatrixClient.sync' event
|
* @param syncData - the data from the 'MatrixClient.sync' event
|
||||||
*/
|
*/
|
||||||
public async onSyncCompleted(syncData: ISyncStateData): Promise<void> {
|
public async onSyncCompleted(syncData: OnSyncCompletedData): Promise<void> {
|
||||||
this.deviceList.setSyncToken(syncData.nextSyncToken ?? null);
|
this.deviceList.setSyncToken(syncData.nextSyncToken ?? null);
|
||||||
this.deviceList.saveIfDirty();
|
this.deviceList.saveIfDirty();
|
||||||
|
|
||||||
@ -3195,6 +3195,21 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
|
||||||
|
// all we do here is filter out encrypted to-device messages with the wrong algorithm. Decryption
|
||||||
|
// happens later in decryptEvent, via the EventMapper
|
||||||
|
return events.filter((toDevice) => {
|
||||||
|
if (
|
||||||
|
toDevice.type === EventType.RoomMessageEncrypted &&
|
||||||
|
!["m.olm.v1.curve25519-aes-sha2"].includes(toDevice.content?.algorithm)
|
||||||
|
) {
|
||||||
|
logger.log("Ignoring invalid encrypted to-device event from " + toDevice.sender);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private onToDeviceEvent = (event: MatrixEvent): void => {
|
private onToDeviceEvent = (event: MatrixEvent): void => {
|
||||||
try {
|
try {
|
||||||
logger.log(
|
logger.log(
|
||||||
@ -3894,5 +3909,5 @@ class IncomingRoomKeyRequestCancellation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IEventDecryptionResult is re-exported for backwards compatibility, in case any applications are referencing it.
|
// a number of types are re-exported for backwards compatibility, in case any applications are referencing it.
|
||||||
export type { IEventDecryptionResult } from "../@types/crypto";
|
export type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||||
|
@ -167,9 +167,9 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
// still has some valuable helper methods that we make use of, so we
|
// still has some valuable helper methods that we make use of, so we
|
||||||
// instantiate it anyways
|
// instantiate it anyways
|
||||||
if (opts.slidingSync) {
|
if (opts.slidingSync) {
|
||||||
this.syncApi = new SlidingSyncSdk(opts.slidingSync, this, opts);
|
this.syncApi = new SlidingSyncSdk(opts.slidingSync, this, opts, this.buildSyncApiOptions());
|
||||||
} else {
|
} else {
|
||||||
this.syncApi = new SyncApi(this, opts);
|
this.syncApi = new SyncApi(this, opts, this.buildSyncApiOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.room = this.syncApi.createRoom(this.roomId);
|
this.room = this.syncApi.createRoom(this.roomId);
|
||||||
|
@ -60,6 +60,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
|||||||
event.setThread(thread);
|
event.setThread(thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: once we get rid of the old libolm-backed crypto, we can restrict this to room events (rather than
|
||||||
|
// to-device events), because the rust implementation decrypts to-device messages at a higher level.
|
||||||
|
// Generally we probably want to use a different eventMapper implementation for to-device events because
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
if (!preventReEmit) {
|
if (!preventReEmit) {
|
||||||
client.reEmitter.reEmit(event, [MatrixEventEvent.Decrypted]);
|
client.reEmitter.reEmit(event, [MatrixEventEvent.Decrypted]);
|
||||||
|
@ -26,6 +26,7 @@ export enum Feature {
|
|||||||
Thread = "Thread",
|
Thread = "Thread",
|
||||||
ThreadUnreadNotifications = "ThreadUnreadNotifications",
|
ThreadUnreadNotifications = "ThreadUnreadNotifications",
|
||||||
LoginTokenRequest = "LoginTokenRequest",
|
LoginTokenRequest = "LoginTokenRequest",
|
||||||
|
RelationBasedRedactions = "RelationBasedRedactions",
|
||||||
AccountDataDeletion = "AccountDataDeletion",
|
AccountDataDeletion = "AccountDataDeletion",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +47,9 @@ const featureSupportResolver: Record<string, FeatureSupportCondition> = {
|
|||||||
[Feature.LoginTokenRequest]: {
|
[Feature.LoginTokenRequest]: {
|
||||||
unstablePrefixes: ["org.matrix.msc3882"],
|
unstablePrefixes: ["org.matrix.msc3882"],
|
||||||
},
|
},
|
||||||
|
[Feature.RelationBasedRedactions]: {
|
||||||
|
unstablePrefixes: ["org.matrix.msc3912"],
|
||||||
|
},
|
||||||
[Feature.AccountDataDeletion]: {
|
[Feature.AccountDataDeletion]: {
|
||||||
unstablePrefixes: ["org.matrix.msc3391"],
|
unstablePrefixes: ["org.matrix.msc3391"],
|
||||||
},
|
},
|
||||||
|
@ -34,11 +34,6 @@ export enum ClientPrefix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum IdentityPrefix {
|
export enum IdentityPrefix {
|
||||||
/**
|
|
||||||
* URI path for v1 of the identity API
|
|
||||||
* @deprecated Use v2.
|
|
||||||
*/
|
|
||||||
V1 = "/_matrix/identity/api/v1",
|
|
||||||
/**
|
/**
|
||||||
* URI path for the v2 identity API
|
* URI path for the v2 identity API
|
||||||
*/
|
*/
|
||||||
|
@ -828,17 +828,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if ((<Error>e).name !== "DecryptionError") {
|
const detailedError = e instanceof DecryptionError ? (<DecryptionError>e).detailedString : String(e);
|
||||||
// not a decryption error: log the whole exception as an error
|
|
||||||
// (and don't bother with a retry)
|
|
||||||
const re = options.isRetry ? "re" : "";
|
|
||||||
// For find results: this can produce "Error decrypting event (id=$ev)" and
|
|
||||||
// "Error redecrypting event (id=$ev)".
|
|
||||||
logger.error(`Error ${re}decrypting event (${this.getDetails()})`, e);
|
|
||||||
this.decryptionPromise = null;
|
|
||||||
this.retryDecryption = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e as Error;
|
err = e as Error;
|
||||||
|
|
||||||
@ -858,10 +848,7 @@ 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(
|
logger.log(`Error decrypting event (${this.getDetails()}), but retrying: ${detailedError}`);
|
||||||
`Error decrypting event (${this.getDetails()}), but retrying: ` +
|
|
||||||
(<DecryptionError>e).detailedString,
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -870,9 +857,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
|||||||
//
|
//
|
||||||
// the detailedString already includes the name and message of the error, and the stack isn't much use,
|
// the detailedString already includes the name and message of the error, and the stack isn't much use,
|
||||||
// so we don't bother to log `e` separately.
|
// so we don't bother to log `e` separately.
|
||||||
logger.warn(`Error decrypting event (${this.getDetails()}): ` + (<DecryptionError>e).detailedString);
|
logger.warn(`Error decrypting event (${this.getDetails()}): ${detailedError}`);
|
||||||
|
|
||||||
res = this.badEncryptedMessage((<DecryptionError>e).message);
|
res = this.badEncryptedMessage(String(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -33,6 +33,9 @@ export type EventHandlerMap = {
|
|||||||
[RelationsEvent.Redaction]: (event: MatrixEvent) => void;
|
[RelationsEvent.Redaction]: (event: MatrixEvent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const matchesEventType = (eventType: string, targetEventType: string, altTargetEventTypes: string[] = []): boolean =>
|
||||||
|
[targetEventType, ...altTargetEventTypes].includes(eventType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A container for relation events that supports easy access to common ways of
|
* A container for relation events that supports easy access to common ways of
|
||||||
* aggregating such events. Each instance holds events that of a single relation
|
* aggregating such events. Each instance holds events that of a single relation
|
||||||
@ -55,11 +58,13 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
|||||||
* @param relationType - The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
|
* @param relationType - The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
|
||||||
* @param eventType - The relation event's type, such as "m.reaction", etc.
|
* @param eventType - The relation event's type, such as "m.reaction", etc.
|
||||||
* @param client - The client which created this instance. For backwards compatibility also accepts a Room.
|
* @param client - The client which created this instance. For backwards compatibility also accepts a Room.
|
||||||
|
* @param altEventTypes - alt event types for relation events, for example to support unstable prefixed event types
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly relationType: RelationType | string,
|
public readonly relationType: RelationType | string,
|
||||||
public readonly eventType: string,
|
public readonly eventType: string,
|
||||||
client: MatrixClient | Room,
|
client: MatrixClient | Room,
|
||||||
|
public readonly altEventTypes?: string[],
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.client = client instanceof Room ? client.client : client;
|
this.client = client instanceof Room ? client.client : client;
|
||||||
@ -84,7 +89,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
|||||||
const relationType = relation.rel_type;
|
const relationType = relation.rel_type;
|
||||||
const eventType = event.getType();
|
const eventType = event.getType();
|
||||||
|
|
||||||
if (this.relationType !== relationType || this.eventType !== eventType) {
|
if (this.relationType !== relationType || !matchesEventType(eventType, this.eventType, this.altEventTypes)) {
|
||||||
logger.error("Event relation info doesn't match this container");
|
logger.error("Event relation info doesn't match this container");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -122,20 +127,6 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const relation = event.getRelation();
|
|
||||||
if (!relation) {
|
|
||||||
logger.error("Event must have relation info");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relationType = relation.rel_type;
|
|
||||||
const eventType = event.getType();
|
|
||||||
|
|
||||||
if (this.relationType !== relationType || this.eventType !== eventType) {
|
|
||||||
logger.error("Event relation info doesn't match this container");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.relations.delete(event);
|
this.relations.delete(event);
|
||||||
|
|
||||||
if (this.relationType === RelationType.Annotation) {
|
if (this.relationType === RelationType.Annotation) {
|
||||||
|
@ -623,6 +623,16 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
return this.oobMemberFlags.status === OobStatus.NotStarted;
|
return this.oobMemberFlags.status === OobStatus.NotStarted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if loading of out-of-band-members has completed
|
||||||
|
*
|
||||||
|
* @returns true if the full membership list of this room has been loaded. False if it is not started or is in
|
||||||
|
* progress.
|
||||||
|
*/
|
||||||
|
public outOfBandMembersReady(): boolean {
|
||||||
|
return this.oobMemberFlags.status === OobStatus.Finished;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark this room state as waiting for out-of-band members,
|
* Mark this room state as waiting for out-of-band members,
|
||||||
* ensuring it doesn't ask for them to be requested again
|
* ensuring it doesn't ask for them to be requested again
|
||||||
|
@ -54,7 +54,13 @@ import {
|
|||||||
FILTER_RELATED_BY_SENDERS,
|
FILTER_RELATED_BY_SENDERS,
|
||||||
ThreadFilterType,
|
ThreadFilterType,
|
||||||
} from "./thread";
|
} from "./thread";
|
||||||
import { MAIN_ROOM_TIMELINE, Receipt, ReceiptContent, ReceiptType } from "../@types/read_receipts";
|
import {
|
||||||
|
CachedReceiptStructure,
|
||||||
|
MAIN_ROOM_TIMELINE,
|
||||||
|
Receipt,
|
||||||
|
ReceiptContent,
|
||||||
|
ReceiptType,
|
||||||
|
} from "../@types/read_receipts";
|
||||||
import { IStateEventWithRoomId } from "../@types/search";
|
import { IStateEventWithRoomId } from "../@types/search";
|
||||||
import { RelationsContainer } from "./relations-container";
|
import { RelationsContainer } from "./relations-container";
|
||||||
import { ReadReceipt, synthesizeReceipt } from "./read-receipt";
|
import { ReadReceipt, synthesizeReceipt } from "./read-receipt";
|
||||||
@ -302,7 +308,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
|
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
|
||||||
private notificationCounts: NotificationCount = {};
|
private notificationCounts: NotificationCount = {};
|
||||||
private readonly threadNotifications = new Map<string, NotificationCount>();
|
private readonly threadNotifications = new Map<string, NotificationCount>();
|
||||||
public readonly cachedThreadReadReceipts = new Map<string, { event: MatrixEvent; synthetic: boolean }[]>();
|
public readonly cachedThreadReadReceipts = new Map<string, CachedReceiptStructure[]>();
|
||||||
private readonly timelineSets: EventTimelineSet[];
|
private readonly timelineSets: EventTimelineSet[];
|
||||||
public readonly threadsTimelineSets: EventTimelineSet[] = [];
|
public readonly threadsTimelineSets: EventTimelineSet[] = [];
|
||||||
// any filtered timeline sets we're maintaining for this room
|
// any filtered timeline sets we're maintaining for this room
|
||||||
@ -441,9 +447,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
});
|
});
|
||||||
events.forEach(async (serializedEvent: Partial<IEvent>) => {
|
events.forEach(async (serializedEvent: Partial<IEvent>) => {
|
||||||
const event = mapper(serializedEvent);
|
const event = mapper(serializedEvent);
|
||||||
if (event.getType() === EventType.RoomMessageEncrypted && this.client.isCryptoEnabled()) {
|
await client.decryptEventIfNeeded(event);
|
||||||
await event.attemptDecryption(this.client.crypto!);
|
|
||||||
}
|
|
||||||
event.setStatus(EventStatus.NOT_SENT);
|
event.setStatus(EventStatus.NOT_SENT);
|
||||||
this.addPendingEvent(event, event.getTxnId()!);
|
this.addPendingEvent(event, event.getTxnId()!);
|
||||||
});
|
});
|
||||||
@ -503,9 +507,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
|
|
||||||
const decryptionPromises = events
|
const decryptionPromises = events
|
||||||
.slice(readReceiptTimelineIndex)
|
.slice(readReceiptTimelineIndex)
|
||||||
.filter((event) => event.shouldAttemptDecryption())
|
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((event) => event.attemptDecryption(this.client.crypto!, { isRetry: true }));
|
.map((event) => this.client.decryptEventIfNeeded(event, { isRetry: true }));
|
||||||
|
|
||||||
await Promise.allSettled(decryptionPromises);
|
await Promise.allSettled(decryptionPromises);
|
||||||
}
|
}
|
||||||
@ -521,9 +524,9 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
const decryptionPromises = this.getUnfilteredTimelineSet()
|
const decryptionPromises = this.getUnfilteredTimelineSet()
|
||||||
.getLiveTimeline()
|
.getLiveTimeline()
|
||||||
.getEvents()
|
.getEvents()
|
||||||
.filter((event) => event.shouldAttemptDecryption())
|
.slice(0) // copy before reversing
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((event) => event.attemptDecryption(this.client.crypto!, { isRetry: true }));
|
.map((event) => this.client.decryptEventIfNeeded(event, { isRetry: true }));
|
||||||
|
|
||||||
await Promise.allSettled(decryptionPromises);
|
await Promise.allSettled(decryptionPromises);
|
||||||
}
|
}
|
||||||
@ -888,6 +891,20 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
return { memberEvents, fromServer };
|
return { memberEvents, fromServer };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if loading of out-of-band-members has completed
|
||||||
|
*
|
||||||
|
* @returns true if the full membership list of this room has been loaded (including if lazy-loading is disabled).
|
||||||
|
* False if the load is not started or is in progress.
|
||||||
|
*/
|
||||||
|
public membersLoaded(): boolean {
|
||||||
|
if (!this.opts.lazyLoadMembers) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.currentState.outOfBandMembersReady();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preloads the member list in case lazy loading
|
* Preloads the member list in case lazy loading
|
||||||
* of memberships is in use. Can be called multiple times,
|
* of memberships is in use. Can be called multiple times,
|
||||||
@ -909,10 +926,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
const inMemoryUpdate = this.loadMembers()
|
const inMemoryUpdate = this.loadMembers()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.currentState.setOutOfBandMembers(result.memberEvents);
|
this.currentState.setOutOfBandMembers(result.memberEvents);
|
||||||
// now the members are loaded, start to track the e2e devices if needed
|
|
||||||
if (this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId)) {
|
|
||||||
this.client.crypto!.trackRoomDevices(this.roomId);
|
|
||||||
}
|
|
||||||
return result.fromServer;
|
return result.fromServer;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -2711,7 +2724,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
// when the thread is created
|
// when the thread is created
|
||||||
this.cachedThreadReadReceipts.set(receipt.thread_id!, [
|
this.cachedThreadReadReceipts.set(receipt.thread_id!, [
|
||||||
...(this.cachedThreadReadReceipts.get(receipt.thread_id!) ?? []),
|
...(this.cachedThreadReadReceipts.get(receipt.thread_id!) ?? []),
|
||||||
{ event, synthetic },
|
{ eventId, receiptType, userId, receipt, synthetic },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -27,7 +27,7 @@ import { RoomState } from "./room-state";
|
|||||||
import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { ReadReceipt } from "./read-receipt";
|
import { ReadReceipt } from "./read-receipt";
|
||||||
import { Receipt, ReceiptContent, ReceiptType } from "../@types/read_receipts";
|
import { CachedReceiptStructure, ReceiptType } from "../@types/read_receipts";
|
||||||
|
|
||||||
export enum ThreadEvent {
|
export enum ThreadEvent {
|
||||||
New = "Thread.new",
|
New = "Thread.new",
|
||||||
@ -50,7 +50,7 @@ interface IThreadOpts {
|
|||||||
room: Room;
|
room: Room;
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
pendingEventOrdering?: PendingEventOrdering;
|
pendingEventOrdering?: PendingEventOrdering;
|
||||||
receipts?: { event: MatrixEvent; synthetic: boolean }[];
|
receipts?: CachedReceiptStructure[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FeatureSupport {
|
export enum FeatureSupport {
|
||||||
@ -97,6 +97,11 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
private readonly pendingEventOrdering: PendingEventOrdering;
|
private readonly pendingEventOrdering: PendingEventOrdering;
|
||||||
|
|
||||||
public initialEventsFetched = !Thread.hasServerSideSupport;
|
public initialEventsFetched = !Thread.hasServerSideSupport;
|
||||||
|
/**
|
||||||
|
* An array of events to add to the timeline once the thread has been initialised
|
||||||
|
* with server suppport.
|
||||||
|
*/
|
||||||
|
public replayEvents: MatrixEvent[] | null = [];
|
||||||
|
|
||||||
public constructor(public readonly id: string, public rootEvent: MatrixEvent | undefined, opts: IThreadOpts) {
|
public constructor(public readonly id: string, public rootEvent: MatrixEvent | undefined, opts: IThreadOpts) {
|
||||||
super();
|
super();
|
||||||
@ -266,6 +271,20 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
this.addEventToTimeline(event, false);
|
this.addEventToTimeline(event, false);
|
||||||
this.fetchEditsWhereNeeded(event);
|
this.fetchEditsWhereNeeded(event);
|
||||||
} else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) {
|
} else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) {
|
||||||
|
if (!this.initialEventsFetched) {
|
||||||
|
/**
|
||||||
|
* A thread can be fully discovered via a single sync response
|
||||||
|
* And when that's the case we still ask the server to do an initialisation
|
||||||
|
* as it's the safest to ensure we have everything.
|
||||||
|
* However when we are in that scenario we might loose annotation or edits
|
||||||
|
*
|
||||||
|
* This fix keeps a reference to those events and replay them once the thread
|
||||||
|
* has been initialised properly.
|
||||||
|
*/
|
||||||
|
this.replayEvents?.push(event);
|
||||||
|
} else {
|
||||||
|
this.addEventToTimeline(event, toStartOfTimeline);
|
||||||
|
}
|
||||||
// Apply annotations and replace relations to the relations of the timeline only
|
// Apply annotations and replace relations to the relations of the timeline only
|
||||||
this.timelineSet.relations?.aggregateParentEvent(event);
|
this.timelineSet.relations?.aggregateParentEvent(event);
|
||||||
this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
|
this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
|
||||||
@ -298,17 +317,9 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
* and apply them to the current thread
|
* and apply them to the current thread
|
||||||
* @param receipts - A collection of the receipts cached from initial sync
|
* @param receipts - A collection of the receipts cached from initial sync
|
||||||
*/
|
*/
|
||||||
private processReceipts(receipts: { event: MatrixEvent; synthetic: boolean }[] = []): void {
|
private processReceipts(receipts: CachedReceiptStructure[] = []): void {
|
||||||
for (const { event, synthetic } of receipts) {
|
for (const { eventId, receiptType, userId, receipt, synthetic } of receipts) {
|
||||||
const content = event.getContent<ReceiptContent>();
|
this.addReceiptToStructure(eventId, receiptType as ReceiptType, userId, receipt, synthetic);
|
||||||
Object.keys(content).forEach((eventId: string) => {
|
|
||||||
Object.keys(content[eventId]).forEach((receiptType: ReceiptType | string) => {
|
|
||||||
Object.keys(content[eventId][receiptType]).forEach((userId: string) => {
|
|
||||||
const receipt = content[eventId][receiptType][userId] as Receipt;
|
|
||||||
this.addReceiptToStructure(eventId, receiptType as ReceiptType, userId, receipt, synthetic);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,6 +386,10 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
limit: Math.max(1, this.length),
|
limit: Math.max(1, this.length),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
for (const event of this.replayEvents!) {
|
||||||
|
this.addEvent(event, false);
|
||||||
|
}
|
||||||
|
this.replayEvents = null;
|
||||||
// just to make sure that, if we've created a timeline window for this thread before the thread itself
|
// just to make sure that, if we've created a timeline window for this thread before the thread itself
|
||||||
// existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
|
// existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
|
||||||
this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true);
|
this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true);
|
||||||
|
@ -20,8 +20,13 @@ import { RustCrypto } from "./rust-crypto";
|
|||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { CryptoBackend } from "../common-crypto/CryptoBackend";
|
import { CryptoBackend } from "../common-crypto/CryptoBackend";
|
||||||
import { RUST_SDK_STORE_PREFIX } from "./constants";
|
import { RUST_SDK_STORE_PREFIX } from "./constants";
|
||||||
|
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
||||||
|
|
||||||
export async function initRustCrypto(userId: string, deviceId: string): Promise<CryptoBackend> {
|
export async function initRustCrypto(
|
||||||
|
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||||
|
userId: string,
|
||||||
|
deviceId: string,
|
||||||
|
): Promise<CryptoBackend> {
|
||||||
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
|
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
|
||||||
await RustSdkCryptoJs.initAsync();
|
await RustSdkCryptoJs.initAsync();
|
||||||
|
|
||||||
@ -34,7 +39,7 @@ export async function initRustCrypto(userId: string, deviceId: string): Promise<
|
|||||||
|
|
||||||
// TODO: use the pickle key for the passphrase
|
// TODO: use the pickle key for the passphrase
|
||||||
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, RUST_SDK_STORE_PREFIX, "test pass");
|
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, RUST_SDK_STORE_PREFIX, "test pass");
|
||||||
const rustCrypto = new RustCrypto(olmMachine, userId, deviceId);
|
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId);
|
||||||
|
|
||||||
logger.info("Completed rust crypto-sdk setup");
|
logger.info("Completed rust crypto-sdk setup");
|
||||||
return rustCrypto;
|
return rustCrypto;
|
||||||
|
@ -15,12 +15,29 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||||
|
import {
|
||||||
|
KeysBackupRequest,
|
||||||
|
KeysClaimRequest,
|
||||||
|
KeysQueryRequest,
|
||||||
|
KeysUploadRequest,
|
||||||
|
SignatureUploadRequest,
|
||||||
|
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||||
|
|
||||||
import { IEventDecryptionResult } from "../@types/crypto";
|
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||||
|
import type { IToDeviceEvent } from "../sync-accumulator";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
import { CryptoBackend } from "../common-crypto/CryptoBackend";
|
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
|
||||||
|
import { QueryDict } from "../utils";
|
||||||
|
|
||||||
// import { logger } from "../logger";
|
/**
|
||||||
|
* Common interface for all the request types returned by `OlmMachine.outgoingRequests`.
|
||||||
|
*/
|
||||||
|
interface OutgoingRequest {
|
||||||
|
readonly id: string | undefined;
|
||||||
|
readonly type: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||||
@ -29,10 +46,18 @@ export class RustCrypto implements CryptoBackend {
|
|||||||
public globalBlacklistUnverifiedDevices = false;
|
public globalBlacklistUnverifiedDevices = false;
|
||||||
public globalErrorOnUnknownDevices = false;
|
public globalErrorOnUnknownDevices = false;
|
||||||
|
|
||||||
/** whether stop() has been called */
|
/** whether {@link stop} has been called */
|
||||||
private stopped = false;
|
private stopped = false;
|
||||||
|
|
||||||
public constructor(private readonly olmMachine: RustSdkCryptoJs.OlmMachine, _userId: string, _deviceId: string) {}
|
/** whether {@link outgoingRequestLoop} is currently running */
|
||||||
|
private outgoingRequestLoopRunning = false;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly olmMachine: RustSdkCryptoJs.OlmMachine,
|
||||||
|
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||||
|
_userId: string,
|
||||||
|
_deviceId: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
public stop(): void {
|
public stop(): void {
|
||||||
// stop() may be called multiple times, but attempting to close() the OlmMachine twice
|
// stop() may be called multiple times, but attempting to close() the OlmMachine twice
|
||||||
@ -57,4 +82,120 @@ export class RustCrypto implements CryptoBackend {
|
|||||||
// TODO
|
// TODO
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async exportRoomKeys(): Promise<IMegolmSessionData[]> {
|
||||||
|
// TODO
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// SyncCryptoCallbacks implementation
|
||||||
|
//
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/** called by the sync loop to preprocess incoming to-device messages
|
||||||
|
*
|
||||||
|
* @param events - the received to-device messages
|
||||||
|
* @returns A list of preprocessed to-device messages.
|
||||||
|
*/
|
||||||
|
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
|
||||||
|
// send the received to-device messages into receiveSyncChanges. We have no info on device-list changes,
|
||||||
|
// one-time-keys, or fallback keys, so just pass empty data.
|
||||||
|
const result = await this.olmMachine.receiveSyncChanges(
|
||||||
|
JSON.stringify(events),
|
||||||
|
new RustSdkCryptoJs.DeviceLists(),
|
||||||
|
new Map(),
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// receiveSyncChanges returns a JSON-encoded list of decrypted to-device messages.
|
||||||
|
return JSON.parse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** called by the sync loop after processing each sync.
|
||||||
|
*
|
||||||
|
* TODO: figure out something equivalent for sliding sync.
|
||||||
|
*
|
||||||
|
* @param syncState - information on the completed sync.
|
||||||
|
*/
|
||||||
|
public onSyncCompleted(syncState: OnSyncCompletedData): void {
|
||||||
|
// Processing the /sync may have produced new outgoing requests which need sending, so kick off the outgoing
|
||||||
|
// request loop, if it's not already running.
|
||||||
|
this.outgoingRequestLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Outgoing requests
|
||||||
|
//
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private async outgoingRequestLoop(): Promise<void> {
|
||||||
|
if (this.outgoingRequestLoopRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.outgoingRequestLoopRunning = true;
|
||||||
|
try {
|
||||||
|
while (!this.stopped) {
|
||||||
|
const outgoingRequests: Object[] = await this.olmMachine.outgoingRequests();
|
||||||
|
if (outgoingRequests.length == 0 || this.stopped) {
|
||||||
|
// no more messages to send (or we have been told to stop): exit the loop
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const msg of outgoingRequests) {
|
||||||
|
await this.doOutgoingRequest(msg as OutgoingRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Error processing outgoing-message requests from rust crypto-sdk", e);
|
||||||
|
} finally {
|
||||||
|
this.outgoingRequestLoopRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doOutgoingRequest(msg: OutgoingRequest): Promise<void> {
|
||||||
|
let resp: string;
|
||||||
|
|
||||||
|
/* refer https://docs.rs/matrix-sdk-crypto/0.6.0/matrix_sdk_crypto/requests/enum.OutgoingRequests.html
|
||||||
|
* for the complete list of request types
|
||||||
|
*/
|
||||||
|
if (msg instanceof KeysUploadRequest) {
|
||||||
|
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body);
|
||||||
|
} else if (msg instanceof KeysQueryRequest) {
|
||||||
|
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/query", {}, msg.body);
|
||||||
|
} else if (msg instanceof KeysClaimRequest) {
|
||||||
|
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/claim", {}, msg.body);
|
||||||
|
} else if (msg instanceof SignatureUploadRequest) {
|
||||||
|
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body);
|
||||||
|
} else if (msg instanceof KeysBackupRequest) {
|
||||||
|
resp = await this.rawJsonRequest(Method.Put, "/_matrix/client/v3/room_keys/keys", {}, msg.body);
|
||||||
|
} else {
|
||||||
|
// TODO: ToDeviceRequest, RoomMessageRequest
|
||||||
|
logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg));
|
||||||
|
resp = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.id) {
|
||||||
|
await this.olmMachine.markRequestAsSent(msg.id, msg.type, resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async rawJsonRequest(method: Method, path: string, queryParams: QueryDict, body: string): Promise<string> {
|
||||||
|
const opts = {
|
||||||
|
// inhibit the JSON stringification and parsing within HttpApi.
|
||||||
|
json: false,
|
||||||
|
|
||||||
|
// nevertheless, we are sending, and accept, JSON.
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
|
||||||
|
// we use the full prefix
|
||||||
|
prefix: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.http.authedRequest<string>(method, path, queryParams, body, opts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,20 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend";
|
||||||
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import * as utils from "./utils";
|
import * as utils from "./utils";
|
||||||
import { EventTimeline } from "./models/event-timeline";
|
import { EventTimeline } from "./models/event-timeline";
|
||||||
import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
|
import { ClientEvent, IStoredClientOpts, MatrixClient } from "./client";
|
||||||
import { ISyncStateData, SyncState, _createAndReEmitRoom } from "./sync";
|
import {
|
||||||
|
ISyncStateData,
|
||||||
|
SyncState,
|
||||||
|
_createAndReEmitRoom,
|
||||||
|
SyncApiOptions,
|
||||||
|
defaultClientOpts,
|
||||||
|
defaultSyncApiOpts,
|
||||||
|
} from "./sync";
|
||||||
import { MatrixEvent } from "./models/event";
|
import { MatrixEvent } from "./models/event";
|
||||||
import { Crypto } from "./crypto";
|
import { Crypto } from "./crypto";
|
||||||
import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState, ISyncResponse } from "./sync-accumulator";
|
import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState, ISyncResponse } from "./sync-accumulator";
|
||||||
@ -102,6 +110,7 @@ class ExtensionE2EE implements Extension<ExtensionE2EERequest, ExtensionE2EEResp
|
|||||||
Array.isArray(unusedFallbackKeys) && !unusedFallbackKeys.includes("signed_curve25519"),
|
Array.isArray(unusedFallbackKeys) && !unusedFallbackKeys.includes("signed_curve25519"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.crypto.onSyncCompleted({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +128,7 @@ type ExtensionToDeviceResponse = {
|
|||||||
class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, ExtensionToDeviceResponse> {
|
class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, ExtensionToDeviceResponse> {
|
||||||
private nextBatch: string | null = null;
|
private nextBatch: string | null = null;
|
||||||
|
|
||||||
public constructor(private readonly client: MatrixClient) {}
|
public constructor(private readonly client: MatrixClient, private readonly cryptoCallbacks?: SyncCryptoCallbacks) {}
|
||||||
|
|
||||||
public name(): string {
|
public name(): string {
|
||||||
return "to_device";
|
return "to_device";
|
||||||
@ -142,8 +151,12 @@ class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, Extension
|
|||||||
|
|
||||||
public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
|
public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
|
||||||
const cancelledKeyVerificationTxns: string[] = [];
|
const cancelledKeyVerificationTxns: string[] = [];
|
||||||
data.events
|
let events = data["events"] || [];
|
||||||
?.map(this.client.getEventMapper())
|
if (events.length > 0 && this.cryptoCallbacks) {
|
||||||
|
events = await this.cryptoCallbacks.preprocessToDeviceMessages(events);
|
||||||
|
}
|
||||||
|
events
|
||||||
|
.map(this.client.getEventMapper())
|
||||||
.map((toDeviceEvent) => {
|
.map((toDeviceEvent) => {
|
||||||
// map is a cheap inline forEach
|
// map is a cheap inline forEach
|
||||||
// We want to flag m.key.verification.start events as cancelled
|
// We want to flag m.key.verification.start events as cancelled
|
||||||
@ -341,6 +354,8 @@ class ExtensionReceipts implements Extension<ExtensionReceiptsRequest, 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 readonly opts: IStoredClientOpts;
|
||||||
|
private readonly syncOpts: SyncApiOptions;
|
||||||
private syncState: SyncState | null = null;
|
private syncState: SyncState | null = null;
|
||||||
private syncStateData?: ISyncStateData;
|
private syncStateData?: ISyncStateData;
|
||||||
private lastPos: string | null = null;
|
private lastPos: string | null = null;
|
||||||
@ -350,19 +365,11 @@ export class SlidingSyncSdk {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly slidingSync: SlidingSync,
|
private readonly slidingSync: SlidingSync,
|
||||||
private readonly client: MatrixClient,
|
private readonly client: MatrixClient,
|
||||||
private readonly opts: Partial<IStoredClientOpts> = {},
|
opts?: IStoredClientOpts,
|
||||||
|
syncOpts?: SyncApiOptions,
|
||||||
) {
|
) {
|
||||||
this.opts.initialSyncLimit = this.opts.initialSyncLimit ?? 8;
|
this.opts = defaultClientOpts(opts);
|
||||||
this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false;
|
this.syncOpts = defaultSyncApiOpts(syncOpts);
|
||||||
this.opts.pollTimeout = this.opts.pollTimeout || 30 * 1000;
|
|
||||||
this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || PendingEventOrdering.Chronological;
|
|
||||||
this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true;
|
|
||||||
|
|
||||||
if (!opts.canResetEntireTimeline) {
|
|
||||||
opts.canResetEntireTimeline = (_roomId: string): boolean => {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client.getNotifTimelineSet()) {
|
if (client.getNotifTimelineSet()) {
|
||||||
client.reEmitter.reEmit(client.getNotifTimelineSet()!, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
|
client.reEmitter.reEmit(client.getNotifTimelineSet()!, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
|
||||||
@ -371,13 +378,13 @@ export class SlidingSyncSdk {
|
|||||||
this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this));
|
this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this));
|
||||||
this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this));
|
this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this));
|
||||||
const extensions: Extension<any, any>[] = [
|
const extensions: Extension<any, any>[] = [
|
||||||
new ExtensionToDevice(this.client),
|
new ExtensionToDevice(this.client, this.syncOpts.cryptoCallbacks),
|
||||||
new ExtensionAccountData(this.client),
|
new ExtensionAccountData(this.client),
|
||||||
new ExtensionTyping(this.client),
|
new ExtensionTyping(this.client),
|
||||||
new ExtensionReceipts(this.client),
|
new ExtensionReceipts(this.client),
|
||||||
];
|
];
|
||||||
if (this.opts.crypto) {
|
if (this.syncOpts.crypto) {
|
||||||
extensions.push(new ExtensionE2EE(this.opts.crypto));
|
extensions.push(new ExtensionE2EE(this.syncOpts.crypto));
|
||||||
}
|
}
|
||||||
extensions.forEach((ext) => {
|
extensions.forEach((ext) => {
|
||||||
this.slidingSync.registerExtension(ext);
|
this.slidingSync.registerExtension(ext);
|
||||||
@ -697,7 +704,7 @@ export class SlidingSyncSdk {
|
|||||||
if (limited) {
|
if (limited) {
|
||||||
room.resetLiveTimeline(
|
room.resetLiveTimeline(
|
||||||
roomData.prev_batch,
|
roomData.prev_batch,
|
||||||
null, // TODO this.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken,
|
null, // TODO this.syncOpts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
// We have to assume any gap in any timeline is
|
// We have to assume any gap in any timeline is
|
||||||
@ -729,8 +736,8 @@ export class SlidingSyncSdk {
|
|||||||
|
|
||||||
const processRoomEvent = async (e: MatrixEvent): Promise<void> => {
|
const processRoomEvent = async (e: MatrixEvent): Promise<void> => {
|
||||||
client.emit(ClientEvent.Event, e);
|
client.emit(ClientEvent.Event, e);
|
||||||
if (e.isState() && e.getType() == EventType.RoomEncryption && this.opts.crypto) {
|
if (e.isState() && e.getType() == EventType.RoomEncryption && this.syncOpts.crypto) {
|
||||||
await this.opts.crypto.onCryptoEvent(room, e);
|
await this.syncOpts.crypto.onCryptoEvent(room, e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -397,6 +397,10 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
|||||||
* @param sub - The subscription information.
|
* @param sub - The subscription information.
|
||||||
*/
|
*/
|
||||||
public addCustomSubscription(name: string, sub: MSC3575RoomSubscription): void {
|
public addCustomSubscription(name: string, sub: MSC3575RoomSubscription): void {
|
||||||
|
if (this.customSubscriptions.has(name)) {
|
||||||
|
logger.warn(`addCustomSubscription: ${name} already exists as a custom subscription, ignoring.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.customSubscriptions.set(name, sub);
|
this.customSubscriptions.set(name, sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,6 +412,11 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
|||||||
* will be used.
|
* will be used.
|
||||||
*/
|
*/
|
||||||
public useCustomSubscription(roomId: string, name: string): void {
|
public useCustomSubscription(roomId: string, name: string): void {
|
||||||
|
// We already know about this custom subscription, as it is immutable,
|
||||||
|
// we don't need to unconfirm the subscription.
|
||||||
|
if (this.roomIdToCustomSubscription.get(roomId) === name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.roomIdToCustomSubscription.set(roomId, name);
|
this.roomIdToCustomSubscription.set(roomId, name);
|
||||||
// unconfirm this subscription so a resend() will send it up afresh.
|
// unconfirm this subscription so a resend() will send it up afresh.
|
||||||
this.confirmedRoomSubscriptions.delete(roomId);
|
this.confirmedRoomSubscriptions.delete(roomId);
|
||||||
|
122
src/sync.ts
122
src/sync.ts
@ -25,6 +25,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import { Optional } from "matrix-events-sdk";
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
|
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend";
|
||||||
import { User, UserEvent } from "./models/user";
|
import { User, UserEvent } from "./models/user";
|
||||||
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
||||||
import * as utils from "./utils";
|
import * as utils from "./utils";
|
||||||
@ -34,7 +35,7 @@ import { EventTimeline } from "./models/event-timeline";
|
|||||||
import { PushProcessor } from "./pushprocessor";
|
import { PushProcessor } from "./pushprocessor";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { InvalidStoreError, InvalidStoreState } from "./errors";
|
import { InvalidStoreError, InvalidStoreState } from "./errors";
|
||||||
import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
|
import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering, ResetTimelineCallback } from "./client";
|
||||||
import {
|
import {
|
||||||
IEphemeral,
|
IEphemeral,
|
||||||
IInvitedRoom,
|
IInvitedRoom,
|
||||||
@ -47,6 +48,7 @@ import {
|
|||||||
IStrippedState,
|
IStrippedState,
|
||||||
ISyncResponse,
|
ISyncResponse,
|
||||||
ITimeline,
|
ITimeline,
|
||||||
|
IToDeviceEvent,
|
||||||
} from "./sync-accumulator";
|
} from "./sync-accumulator";
|
||||||
import { MatrixEvent } from "./models/event";
|
import { MatrixEvent } from "./models/event";
|
||||||
import { MatrixError, Method } from "./http-api";
|
import { MatrixError, Method } from "./http-api";
|
||||||
@ -59,6 +61,7 @@ import { BeaconEvent } from "./models/beacon";
|
|||||||
import { IEventsResponse } from "./@types/requests";
|
import { IEventsResponse } from "./@types/requests";
|
||||||
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
|
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
|
||||||
import { Feature, ServerSupport } from "./feature";
|
import { Feature, ServerSupport } from "./feature";
|
||||||
|
import { Crypto } from "./crypto";
|
||||||
|
|
||||||
const DEBUG = true;
|
const DEBUG = true;
|
||||||
|
|
||||||
@ -110,6 +113,31 @@ function debuglog(...params: any[]): void {
|
|||||||
logger.log(...params);
|
logger.log(...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options passed into the constructor of SyncApi by MatrixClient
|
||||||
|
*/
|
||||||
|
export interface SyncApiOptions {
|
||||||
|
/**
|
||||||
|
* Crypto manager
|
||||||
|
*
|
||||||
|
* @deprecated in favour of cryptoCallbacks
|
||||||
|
*/
|
||||||
|
crypto?: Crypto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If crypto is enabled on our client, callbacks into the crypto module
|
||||||
|
*/
|
||||||
|
cryptoCallbacks?: SyncCryptoCallbacks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function which is called
|
||||||
|
* with a room ID and returns a boolean. It should return 'true' if the SDK can
|
||||||
|
* SAFELY remove events from this room. It may not be safe to remove events if
|
||||||
|
* there are other references to the timelines for this room.
|
||||||
|
*/
|
||||||
|
canResetEntireTimeline?: ResetTimelineCallback;
|
||||||
|
}
|
||||||
|
|
||||||
interface ISyncOptions {
|
interface ISyncOptions {
|
||||||
filter?: string;
|
filter?: string;
|
||||||
hasSyncedBefore?: boolean;
|
hasSyncedBefore?: boolean;
|
||||||
@ -163,7 +191,29 @@ type WrappedRoom<T> = T & {
|
|||||||
isBrandNewRoom: boolean;
|
isBrandNewRoom: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** add default settings to an IStoredClientOpts */
|
||||||
|
export function defaultClientOpts(opts?: IStoredClientOpts): IStoredClientOpts {
|
||||||
|
return {
|
||||||
|
initialSyncLimit: 8,
|
||||||
|
resolveInvitesToProfiles: false,
|
||||||
|
pollTimeout: 30 * 1000,
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Chronological,
|
||||||
|
experimentalThreadSupport: false,
|
||||||
|
...opts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultSyncApiOpts(syncOpts?: SyncApiOptions): SyncApiOptions {
|
||||||
|
return {
|
||||||
|
canResetEntireTimeline: (_roomId): boolean => false,
|
||||||
|
...syncOpts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class SyncApi {
|
export class SyncApi {
|
||||||
|
private readonly opts: IStoredClientOpts;
|
||||||
|
private readonly syncOpts: SyncApiOptions;
|
||||||
|
|
||||||
private _peekRoom: Optional<Room> = null;
|
private _peekRoom: Optional<Room> = null;
|
||||||
private currentSyncRequest?: Promise<ISyncResponse>;
|
private currentSyncRequest?: Promise<ISyncResponse>;
|
||||||
private abortController?: AbortController;
|
private abortController?: AbortController;
|
||||||
@ -180,21 +230,13 @@ export class SyncApi {
|
|||||||
/**
|
/**
|
||||||
* Construct an entity which is able to sync with a homeserver.
|
* Construct an entity which is able to sync with a homeserver.
|
||||||
* @param client - The matrix client instance to use.
|
* @param client - The matrix client instance to use.
|
||||||
* @param opts - Config options
|
* @param opts - client config options
|
||||||
|
* @param syncOpts - sync-specific options passed by the client
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
public constructor(private readonly client: MatrixClient, private readonly opts: Partial<IStoredClientOpts> = {}) {
|
public constructor(private readonly client: MatrixClient, opts?: IStoredClientOpts, syncOpts?: SyncApiOptions) {
|
||||||
this.opts.initialSyncLimit = this.opts.initialSyncLimit ?? 8;
|
this.opts = defaultClientOpts(opts);
|
||||||
this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false;
|
this.syncOpts = defaultSyncApiOpts(syncOpts);
|
||||||
this.opts.pollTimeout = this.opts.pollTimeout || 30 * 1000;
|
|
||||||
this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || PendingEventOrdering.Chronological;
|
|
||||||
this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true;
|
|
||||||
|
|
||||||
if (!opts.canResetEntireTimeline) {
|
|
||||||
opts.canResetEntireTimeline = (roomId: string): boolean => {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client.getNotifTimelineSet()) {
|
if (client.getNotifTimelineSet()) {
|
||||||
client.reEmitter.reEmit(client.getNotifTimelineSet()!, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
|
client.reEmitter.reEmit(client.getNotifTimelineSet()!, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
|
||||||
@ -632,7 +674,7 @@ export class SyncApi {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.opts.lazyLoadMembers) {
|
if (this.opts.lazyLoadMembers) {
|
||||||
this.opts.crypto?.enableLazyLoading();
|
this.syncOpts.crypto?.enableLazyLoading();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
debuglog("Storing client options...");
|
debuglog("Storing client options...");
|
||||||
@ -866,10 +908,10 @@ export class SyncApi {
|
|||||||
catchingUp: this.catchingUp,
|
catchingUp: this.catchingUp,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.opts.crypto) {
|
if (this.syncOpts.crypto) {
|
||||||
// tell the crypto module we're about to process a sync
|
// tell the crypto module we're about to process a sync
|
||||||
// response
|
// response
|
||||||
await this.opts.crypto.onSyncWillProcess(syncEventData);
|
await this.syncOpts.crypto.onSyncWillProcess(syncEventData);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -894,8 +936,8 @@ export class SyncApi {
|
|||||||
|
|
||||||
// tell the crypto module to do its processing. It may block (to do a
|
// tell the crypto module to do its processing. It may block (to do a
|
||||||
// /keys/changes request).
|
// /keys/changes request).
|
||||||
if (this.opts.crypto) {
|
if (this.syncOpts.cryptoCallbacks) {
|
||||||
await this.opts.crypto.onSyncCompleted(syncEventData);
|
await this.syncOpts.cryptoCallbacks.onSyncCompleted(syncEventData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
|
// keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
|
||||||
@ -907,8 +949,8 @@ export class SyncApi {
|
|||||||
// stored sync data which means we don't have to worry that we may have missed
|
// stored sync data which means we don't have to worry that we may have missed
|
||||||
// device changes. We can also skip the delay since we're not calling this very
|
// device changes. We can also skip the delay since we're not calling this very
|
||||||
// frequently (and we don't really want to delay the sync for it).
|
// frequently (and we don't really want to delay the sync for it).
|
||||||
if (this.opts.crypto) {
|
if (this.syncOpts.crypto) {
|
||||||
await this.opts.crypto.saveDeviceList(0);
|
await this.syncOpts.crypto.saveDeviceList(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// tell databases that everything is now in a consistent state and can be saved.
|
// tell databases that everything is now in a consistent state and can be saved.
|
||||||
@ -1129,19 +1171,15 @@ 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 (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0) {
|
||||||
const cancelledKeyVerificationTxns: string[] = [];
|
let toDeviceMessages: IToDeviceEvent[] = data.to_device.events;
|
||||||
data.to_device!.events.filter((eventJSON) => {
|
|
||||||
if (
|
|
||||||
eventJSON.type === EventType.RoomMessageEncrypted &&
|
|
||||||
!["m.olm.v1.curve25519-aes-sha2"].includes(eventJSON.content?.algorithm)
|
|
||||||
) {
|
|
||||||
logger.log("Ignoring invalid encrypted to-device event from " + eventJSON.sender);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
if (this.syncOpts.cryptoCallbacks) {
|
||||||
})
|
toDeviceMessages = await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelledKeyVerificationTxns: string[] = [];
|
||||||
|
toDeviceMessages
|
||||||
.map(client.getEventMapper({ toDevice: true }))
|
.map(client.getEventMapper({ toDevice: true }))
|
||||||
.map((toDeviceEvent) => {
|
.map((toDeviceEvent) => {
|
||||||
// map is a cheap inline forEach
|
// map is a cheap inline forEach
|
||||||
@ -1356,7 +1394,7 @@ export class SyncApi {
|
|||||||
if (limited) {
|
if (limited) {
|
||||||
room.resetLiveTimeline(
|
room.resetLiveTimeline(
|
||||||
joinObj.timeline.prev_batch,
|
joinObj.timeline.prev_batch,
|
||||||
this.opts.canResetEntireTimeline!(room.roomId) ? null : syncEventData.oldSyncToken ?? null,
|
this.syncOpts.canResetEntireTimeline!(room.roomId) ? null : syncEventData.oldSyncToken ?? null,
|
||||||
);
|
);
|
||||||
|
|
||||||
// We have to assume any gap in any timeline is
|
// We have to assume any gap in any timeline is
|
||||||
@ -1370,10 +1408,10 @@ export class SyncApi {
|
|||||||
// avoids a race condition if the application tries to send a message after the
|
// avoids a race condition if the application tries to send a message after the
|
||||||
// state event is processed, but before crypto is enabled, which then causes the
|
// state event is processed, but before crypto is enabled, which then causes the
|
||||||
// crypto layer to complain.
|
// crypto layer to complain.
|
||||||
if (this.opts.crypto) {
|
if (this.syncOpts.crypto) {
|
||||||
for (const e of stateEvents.concat(events)) {
|
for (const e of stateEvents.concat(events)) {
|
||||||
if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") {
|
if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") {
|
||||||
await this.opts.crypto.onCryptoEvent(room, e);
|
await this.syncOpts.crypto.onCryptoEvent(room, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1462,8 +1500,8 @@ export class SyncApi {
|
|||||||
|
|
||||||
// Handle device list updates
|
// Handle device list updates
|
||||||
if (data.device_lists) {
|
if (data.device_lists) {
|
||||||
if (this.opts.crypto) {
|
if (this.syncOpts.crypto) {
|
||||||
await this.opts.crypto.handleDeviceListChanges(syncEventData, data.device_lists);
|
await this.syncOpts.crypto.handleDeviceListChanges(syncEventData, data.device_lists);
|
||||||
} else {
|
} else {
|
||||||
// FIXME if we *don't* have a crypto module, we still need to
|
// FIXME if we *don't* have a crypto module, we still need to
|
||||||
// invalidate the device lists. But that would require a
|
// invalidate the device lists. But that would require a
|
||||||
@ -1472,12 +1510,12 @@ export class SyncApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle one_time_keys_count
|
// Handle one_time_keys_count
|
||||||
if (this.opts.crypto && data.device_one_time_keys_count) {
|
if (this.syncOpts.crypto && data.device_one_time_keys_count) {
|
||||||
const currentCount = data.device_one_time_keys_count.signed_curve25519 || 0;
|
const currentCount = data.device_one_time_keys_count.signed_curve25519 || 0;
|
||||||
this.opts.crypto.updateOneTimeKeyCount(currentCount);
|
this.syncOpts.crypto.updateOneTimeKeyCount(currentCount);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
this.opts.crypto &&
|
this.syncOpts.crypto &&
|
||||||
(data.device_unused_fallback_key_types || data["org.matrix.msc2732.device_unused_fallback_key_types"])
|
(data.device_unused_fallback_key_types || data["org.matrix.msc2732.device_unused_fallback_key_types"])
|
||||||
) {
|
) {
|
||||||
// The presence of device_unused_fallback_key_types indicates that the
|
// The presence of device_unused_fallback_key_types indicates that the
|
||||||
@ -1485,7 +1523,7 @@ export class SyncApi {
|
|||||||
// signed_curve25519 fallback key we need a new one.
|
// signed_curve25519 fallback key we need a new one.
|
||||||
const unusedFallbackKeys =
|
const unusedFallbackKeys =
|
||||||
data.device_unused_fallback_key_types || data["org.matrix.msc2732.device_unused_fallback_key_types"];
|
data.device_unused_fallback_key_types || data["org.matrix.msc2732.device_unused_fallback_key_types"];
|
||||||
this.opts.crypto.setNeedsNewFallback(
|
this.syncOpts.crypto.setNeedsNewFallback(
|
||||||
Array.isArray(unusedFallbackKeys) && !unusedFallbackKeys.includes("signed_curve25519"),
|
Array.isArray(unusedFallbackKeys) && !unusedFallbackKeys.includes("signed_curve25519"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2475,18 +2475,20 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
}
|
}
|
||||||
|
|
||||||
private stopAllMedia(): void {
|
private stopAllMedia(): void {
|
||||||
logger.debug(
|
logger.debug(`Call ${this.callId} stopping all media`);
|
||||||
!this.groupCallId
|
|
||||||
? `Call ${this.callId} stopping all media`
|
|
||||||
: `Call ${this.callId} stopping all media except local feeds`,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const feed of this.feeds) {
|
for (const feed of this.feeds) {
|
||||||
if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Usermedia && !this.groupCallId) {
|
// Slightly awkward as local feed need to go via the correct method on
|
||||||
|
// the mediahandler so they get removed from mediahandler (remote tracks
|
||||||
|
// don't)
|
||||||
|
// NB. We clone local streams when passing them to individual calls in a group
|
||||||
|
// call, so we can (and should) stop the clones once we no longer need them:
|
||||||
|
// the other clones will continue fine.
|
||||||
|
if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Usermedia) {
|
||||||
this.client.getMediaHandler().stopUserMediaStream(feed.stream);
|
this.client.getMediaHandler().stopUserMediaStream(feed.stream);
|
||||||
} else if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Screenshare && !this.groupCallId) {
|
} else if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Screenshare) {
|
||||||
this.client.getMediaHandler().stopScreensharingStream(feed.stream);
|
this.client.getMediaHandler().stopScreensharingStream(feed.stream);
|
||||||
} else if (!feed.isLocal() || !this.groupCallId) {
|
} else if (!feed.isLocal()) {
|
||||||
logger.debug("Stopping remote stream", feed.stream.id);
|
logger.debug("Stopping remote stream", feed.stream.id);
|
||||||
for (const track of feed.stream.getTracks()) {
|
for (const track of feed.stream.getTracks()) {
|
||||||
track.stop();
|
track.stop();
|
||||||
|
@ -55,7 +55,7 @@ export enum GroupCallEvent {
|
|||||||
export type GroupCallEventHandlerMap = {
|
export type GroupCallEventHandlerMap = {
|
||||||
[GroupCallEvent.GroupCallStateChanged]: (newState: GroupCallState, oldState: GroupCallState) => void;
|
[GroupCallEvent.GroupCallStateChanged]: (newState: GroupCallState, oldState: GroupCallState) => void;
|
||||||
[GroupCallEvent.ActiveSpeakerChanged]: (activeSpeaker: CallFeed | undefined) => void;
|
[GroupCallEvent.ActiveSpeakerChanged]: (activeSpeaker: CallFeed | undefined) => void;
|
||||||
[GroupCallEvent.CallsChanged]: (calls: Map<RoomMember, Map<string, MatrixCall>>) => void;
|
[GroupCallEvent.CallsChanged]: (calls: Map<string, Map<string, MatrixCall>>) => void;
|
||||||
[GroupCallEvent.UserMediaFeedsChanged]: (feeds: CallFeed[]) => void;
|
[GroupCallEvent.UserMediaFeedsChanged]: (feeds: CallFeed[]) => void;
|
||||||
[GroupCallEvent.ScreenshareFeedsChanged]: (feeds: CallFeed[]) => void;
|
[GroupCallEvent.ScreenshareFeedsChanged]: (feeds: CallFeed[]) => void;
|
||||||
[GroupCallEvent.LocalScreenshareStateChanged]: (
|
[GroupCallEvent.LocalScreenshareStateChanged]: (
|
||||||
@ -197,11 +197,11 @@ export class GroupCall extends TypedEventEmitter<
|
|||||||
public readonly screenshareFeeds: CallFeed[] = [];
|
public readonly screenshareFeeds: CallFeed[] = [];
|
||||||
public groupCallId: string;
|
public groupCallId: string;
|
||||||
|
|
||||||
private readonly calls = new Map<RoomMember, Map<string, MatrixCall>>(); // RoomMember -> device ID -> MatrixCall
|
private readonly calls = new Map<string, Map<string, MatrixCall>>(); // user_id -> device_id -> MatrixCall
|
||||||
private callHandlers = new Map<string, Map<string, ICallHandlers>>(); // User ID -> device ID -> handlers
|
private callHandlers = new Map<string, Map<string, ICallHandlers>>(); // user_id -> device_id -> ICallHandlers
|
||||||
private activeSpeakerLoopInterval?: ReturnType<typeof setTimeout>;
|
private activeSpeakerLoopInterval?: ReturnType<typeof setTimeout>;
|
||||||
private retryCallLoopInterval?: ReturnType<typeof setTimeout>;
|
private retryCallLoopInterval?: ReturnType<typeof setTimeout>;
|
||||||
private retryCallCounts: Map<RoomMember, Map<string, number>> = new Map();
|
private retryCallCounts: Map<string, Map<string, number>> = new Map(); // user_id -> device_id -> count
|
||||||
private reEmitter: ReEmitter;
|
private reEmitter: ReEmitter;
|
||||||
private transmitTimer: ReturnType<typeof setTimeout> | null = null;
|
private transmitTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
private participantsExpirationTimer: ReturnType<typeof setTimeout> | null = null;
|
private participantsExpirationTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
@ -728,18 +728,18 @@ export class GroupCall extends TypedEventEmitter<
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const opponent = newCall.getOpponentMember();
|
const opponentUserId = newCall.getOpponentMember()?.userId;
|
||||||
if (opponent === undefined) {
|
if (opponentUserId === undefined) {
|
||||||
logger.warn("Incoming call with no member. Ignoring.");
|
logger.warn("Incoming call with no member. Ignoring.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceMap = this.calls.get(opponent) ?? new Map<string, MatrixCall>();
|
const deviceMap = this.calls.get(opponentUserId) ?? new Map<string, MatrixCall>();
|
||||||
const prevCall = deviceMap.get(newCall.getOpponentDeviceId()!);
|
const prevCall = deviceMap.get(newCall.getOpponentDeviceId()!);
|
||||||
|
|
||||||
if (prevCall?.callId === newCall.callId) return;
|
if (prevCall?.callId === newCall.callId) return;
|
||||||
|
|
||||||
logger.log(`GroupCall: incoming call from ${opponent.userId} with ID ${newCall.callId}`);
|
logger.log(`GroupCall: incoming call from ${opponentUserId} with ID ${newCall.callId}`);
|
||||||
|
|
||||||
if (prevCall) this.disposeCall(prevCall, CallErrorCode.Replaced);
|
if (prevCall) this.disposeCall(prevCall, CallErrorCode.Replaced);
|
||||||
|
|
||||||
@ -747,7 +747,7 @@ export class GroupCall extends TypedEventEmitter<
|
|||||||
newCall.answerWithCallFeeds(this.getLocalFeeds().map((feed) => feed.clone()));
|
newCall.answerWithCallFeeds(this.getLocalFeeds().map((feed) => feed.clone()));
|
||||||
|
|
||||||
deviceMap.set(newCall.getOpponentDeviceId()!, newCall);
|
deviceMap.set(newCall.getOpponentDeviceId()!, newCall);
|
||||||
this.calls.set(opponent, deviceMap);
|
this.calls.set(opponentUserId, deviceMap);
|
||||||
this.emit(GroupCallEvent.CallsChanged, this.calls);
|
this.emit(GroupCallEvent.CallsChanged, this.calls);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -775,38 +775,38 @@ export class GroupCall extends TypedEventEmitter<
|
|||||||
private placeOutgoingCalls(): void {
|
private placeOutgoingCalls(): void {
|
||||||
let callsChanged = false;
|
let callsChanged = false;
|
||||||
|
|
||||||
for (const [member, participantMap] of this.participants) {
|
for (const [{ userId }, participantMap] of this.participants) {
|
||||||
const callMap = this.calls.get(member) ?? new Map<string, MatrixCall>();
|
const callMap = this.calls.get(userId) ?? new Map<string, MatrixCall>();
|
||||||
|
|
||||||
for (const [deviceId, participant] of participantMap) {
|
for (const [deviceId, participant] of participantMap) {
|
||||||
const prevCall = callMap.get(deviceId);
|
const prevCall = callMap.get(deviceId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
prevCall?.getOpponentSessionId() !== participant.sessionId &&
|
prevCall?.getOpponentSessionId() !== participant.sessionId &&
|
||||||
this.wantsOutgoingCall(member.userId, deviceId)
|
this.wantsOutgoingCall(userId, deviceId)
|
||||||
) {
|
) {
|
||||||
callsChanged = true;
|
callsChanged = true;
|
||||||
|
|
||||||
if (prevCall !== undefined) {
|
if (prevCall !== undefined) {
|
||||||
logger.debug(`Replacing call ${prevCall.callId} to ${member.userId} ${deviceId}`);
|
logger.debug(`Replacing call ${prevCall.callId} to ${userId} ${deviceId}`);
|
||||||
this.disposeCall(prevCall, CallErrorCode.NewSession);
|
this.disposeCall(prevCall, CallErrorCode.NewSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCall = createNewMatrixCall(this.client, this.room.roomId, {
|
const newCall = createNewMatrixCall(this.client, this.room.roomId, {
|
||||||
invitee: member.userId,
|
invitee: userId,
|
||||||
opponentDeviceId: deviceId,
|
opponentDeviceId: deviceId,
|
||||||
opponentSessionId: participant.sessionId,
|
opponentSessionId: participant.sessionId,
|
||||||
groupCallId: this.groupCallId,
|
groupCallId: this.groupCallId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newCall === null) {
|
if (newCall === null) {
|
||||||
logger.error(`Failed to create call with ${member.userId} ${deviceId}`);
|
logger.error(`Failed to create call with ${userId} ${deviceId}`);
|
||||||
callMap.delete(deviceId);
|
callMap.delete(deviceId);
|
||||||
} else {
|
} else {
|
||||||
this.initCall(newCall);
|
this.initCall(newCall);
|
||||||
callMap.set(deviceId, newCall);
|
callMap.set(deviceId, newCall);
|
||||||
|
|
||||||
logger.debug(`Placing call to ${member.userId} ${deviceId} (session ${participant.sessionId})`);
|
logger.debug(`Placing call to ${userId} ${deviceId} (session ${participant.sessionId})`);
|
||||||
|
|
||||||
newCall
|
newCall
|
||||||
.placeCallWithCallFeeds(
|
.placeCallWithCallFeeds(
|
||||||
@ -819,7 +819,7 @@ export class GroupCall extends TypedEventEmitter<
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
logger.warn(`Failed to place call to ${member.userId}`, e);
|
logger.warn(`Failed to place call to ${userId}`, e);
|
||||||
|
|
||||||
if (e instanceof CallError && e.code === GroupCallErrorCode.UnknownDevice) {
|
if (e instanceof CallError && e.code === GroupCallErrorCode.UnknownDevice) {
|
||||||
this.emit(GroupCallEvent.Error, e);
|
this.emit(GroupCallEvent.Error, e);
|
||||||
@ -828,7 +828,7 @@ export class GroupCall extends TypedEventEmitter<
|
|||||||
GroupCallEvent.Error,
|
GroupCallEvent.Error,
|
||||||
new GroupCallError(
|
new GroupCallError(
|
||||||
GroupCallErrorCode.PlaceCallFailed,
|
GroupCallErrorCode.PlaceCallFailed,
|
||||||
`Failed to place call to ${member.userId}`,
|
`Failed to place call to ${userId}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -841,9 +841,9 @@ export class GroupCall extends TypedEventEmitter<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (callMap.size > 0) {
|
if (callMap.size > 0) {
|
||||||
this.calls.set(member, callMap);
|
this.calls.set(userId, callMap);
|
||||||
} else {
|
} else {
|
||||||
this.calls.delete(member);
|
this.calls.delete(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -865,9 +865,9 @@ export class GroupCall extends TypedEventEmitter<
|
|||||||
private onRetryCallLoop = (): void => {
|
private onRetryCallLoop = (): void => {
|
||||||
let needsRetry = false;
|
let needsRetry = false;
|
||||||
|
|
||||||
for (const [member, participantMap] of this.participants) {
|
for (const [{ userId }, participantMap] of this.participants) {
|
||||||
const callMap = this.calls.get(member);
|
const callMap = this.calls.get(userId);
|
||||||
let retriesMap = this.retryCallCounts.get(member);
|
let retriesMap = this.retryCallCounts.get(userId);
|
||||||
|
|
||||||
for (const [deviceId, participant] of participantMap) {
|
for (const [deviceId, participant] of participantMap) {
|
||||||
const call = callMap?.get(deviceId);
|
const call = callMap?.get(deviceId);
|
||||||
@ -875,12 +875,12 @@ export class GroupCall extends TypedEventEmitter<
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
call?.getOpponentSessionId() !== participant.sessionId &&
|
call?.getOpponentSessionId() !== participant.sessionId &&
|
||||||
this.wantsOutgoingCall(member.userId, deviceId) &&
|
this.wantsOutgoingCall(userId, deviceId) &&
|
||||||
retries < 3
|
retries < 3
|
||||||
) {
|
) {
|
||||||
if (retriesMap === undefined) {
|
if (retriesMap === undefined) {
|
||||||
retriesMap = new Map();
|
retriesMap = new Map();
|
||||||
this.retryCallCounts.set(member, retriesMap);
|
this.retryCallCounts.set(userId, retriesMap);
|
||||||
}
|
}
|
||||||
retriesMap.set(deviceId, retries + 1);
|
retriesMap.set(deviceId, retries + 1);
|
||||||
needsRetry = true;
|
needsRetry = true;
|
||||||
@ -1020,36 +1020,36 @@ export class GroupCall extends TypedEventEmitter<
|
|||||||
call.setLocalVideoMuted(videoMuted);
|
call.setLocalVideoMuted(videoMuted);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state === CallState.Connected) {
|
const opponentUserId = call.getOpponentMember()?.userId;
|
||||||
const opponent = call.getOpponentMember()!;
|
if (state === CallState.Connected && opponentUserId) {
|
||||||
const retriesMap = this.retryCallCounts.get(opponent);
|
const retriesMap = this.retryCallCounts.get(opponentUserId);
|
||||||
retriesMap?.delete(call.getOpponentDeviceId()!);
|
retriesMap?.delete(call.getOpponentDeviceId()!);
|
||||||
if (retriesMap?.size === 0) this.retryCallCounts.delete(opponent);
|
if (retriesMap?.size === 0) this.retryCallCounts.delete(opponentUserId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCallHangup = (call: MatrixCall): void => {
|
private onCallHangup = (call: MatrixCall): void => {
|
||||||
if (call.hangupReason === CallErrorCode.Replaced) return;
|
if (call.hangupReason === CallErrorCode.Replaced) return;
|
||||||
|
|
||||||
const opponent = call.getOpponentMember() ?? this.room.getMember(call.invitee!)!;
|
const opponentUserId = call.getOpponentMember()?.userId ?? this.room.getMember(call.invitee!)!.userId;
|
||||||
const deviceMap = this.calls.get(opponent);
|
const deviceMap = this.calls.get(opponentUserId);
|
||||||
|
|
||||||
// Sanity check that this call is in fact in the map
|
// Sanity check that this call is in fact in the map
|
||||||
if (deviceMap?.get(call.getOpponentDeviceId()!) === call) {
|
if (deviceMap?.get(call.getOpponentDeviceId()!) === call) {
|
||||||
this.disposeCall(call, call.hangupReason as CallErrorCode);
|
this.disposeCall(call, call.hangupReason as CallErrorCode);
|
||||||
deviceMap.delete(call.getOpponentDeviceId()!);
|
deviceMap.delete(call.getOpponentDeviceId()!);
|
||||||
if (deviceMap.size === 0) this.calls.delete(opponent);
|
if (deviceMap.size === 0) this.calls.delete(opponentUserId);
|
||||||
this.emit(GroupCallEvent.CallsChanged, this.calls);
|
this.emit(GroupCallEvent.CallsChanged, this.calls);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCallReplaced = (prevCall: MatrixCall, newCall: MatrixCall): void => {
|
private onCallReplaced = (prevCall: MatrixCall, newCall: MatrixCall): void => {
|
||||||
const opponent = prevCall.getOpponentMember()!;
|
const opponentUserId = prevCall.getOpponentMember()!.userId;
|
||||||
|
|
||||||
let deviceMap = this.calls.get(opponent);
|
let deviceMap = this.calls.get(opponentUserId);
|
||||||
if (deviceMap === undefined) {
|
if (deviceMap === undefined) {
|
||||||
deviceMap = new Map();
|
deviceMap = new Map();
|
||||||
this.calls.set(opponent, deviceMap);
|
this.calls.set(opponentUserId, deviceMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.disposeCall(prevCall, CallErrorCode.Replaced);
|
this.disposeCall(prevCall, CallErrorCode.Replaced);
|
||||||
|
Reference in New Issue
Block a user