Skip to content

Commit 07ca367

Browse files
authored
Merge branch 'develop' into poljar/issue-23792
2 parents d507477 + 1606274 commit 07ca367

29 files changed

+1387
-813
lines changed

.github/CODEOWNERS

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
* @matrix-org/element-web
2-
3-
/src/webrtc @matrix-org/element-call-reviewers
4-
/spec/*/webrtc @matrix-org/element-call-reviewers
1+
* @matrix-org/element-web
2+
/.github/workflows/** @matrix-org/element-web-app-team
3+
/package.json @matrix-org/element-web-app-team
4+
/yarn.lock @matrix-org/element-web-app-team
5+
/src/webrtc @matrix-org/element-call-reviewers
6+
/spec/*/webrtc @matrix-org/element-call-reviewers

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@
99
Matrix Javascript SDK
1010
=====================
1111

12-
This is the [Matrix](https://matrix.org) Client-Server r0 SDK for
13-
JavaScript. This SDK can be run in a browser or in Node.js.
12+
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
13+
browser or in Node.js.
14+
15+
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only
16+
guarantees that a feature will be supported for at least 4 spec releases. For example, if a feature the js-sdk supports
17+
is removed in v1.4 then the feature is *eligible* for removal from the SDK when v1.8 is released. This SDK has no
18+
guarantee on implementing all features of any particular spec release, currently. This can mean that the SDK will call
19+
endpoints from before Matrix 1.1, for example.
1420

1521
Quickstart
1622
==========

spec/integ/matrix-client-methods.spec.ts

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,9 @@ describe("MatrixClient", function() {
173173
signatures: {},
174174
};
175175

176-
httpBackend!.when("POST", inviteSignUrl).respond(200, signature);
176+
httpBackend!.when("POST", inviteSignUrl).check(request => {
177+
expect(request.queryParams?.mxid).toEqual(client!.getUserId());
178+
}).respond(200, signature);
177179
httpBackend!.when("POST", "/join/" + encodeURIComponent(roomId)).check(request => {
178180
expect(request.data.third_party_signed).toEqual(signature);
179181
}).respond(200, { room_id: roomId });
@@ -1335,27 +1337,38 @@ describe("MatrixClient", function() {
13351337
});
13361338
});
13371339

1338-
describe("registerWithIdentityServer", () => {
1339-
it("should pass data to POST request", async () => {
1340-
const token = {
1341-
access_token: "access_token",
1342-
token_type: "Bearer",
1343-
matrix_server_name: "server_name",
1344-
expires_in: 12345,
1345-
};
1340+
describe("setPowerLevel", () => {
1341+
it.each([
1342+
{
1343+
userId: "alice@localhost",
1344+
expectation: {
1345+
"alice@localhost": 100,
1346+
},
1347+
},
1348+
{
1349+
userId: ["alice@localhost", "bob@localhost"],
1350+
expectation: {
1351+
"alice@localhost": 100,
1352+
"bob@localhost": 100,
1353+
},
1354+
},
1355+
])("should modify power levels of $userId correctly", async ({ userId, expectation }) => {
1356+
const event = {
1357+
getType: () => "m.room.power_levels",
1358+
getContent: () => ({
1359+
users: {
1360+
"alice@localhost": 50,
1361+
},
1362+
}),
1363+
} as MatrixEvent;
13461364

1347-
httpBackend!.when("POST", "/account/register").check(req => {
1348-
expect(req.data).toStrictEqual(token);
1349-
}).respond(200, {
1350-
access_token: "at",
1351-
token: "tt",
1352-
});
1365+
httpBackend!.when("PUT", "/state/m.room.power_levels").check(req => {
1366+
expect(req.data.users).toStrictEqual(expectation);
1367+
}).respond(200, {});
13531368

1354-
const prom = client!.registerWithIdentityServer(token);
1369+
const prom = client!.setPowerLevel("!room_id:server", userId, 100, event);
13551370
await httpBackend!.flushAllExpected();
1356-
const resp = await prom;
1357-
expect(resp.access_token).toBe("at");
1358-
expect(resp.token).toBe("tt");
1371+
await prom;
13591372
});
13601373
});
13611374
});

spec/integ/megolm-integ.spec.ts

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,19 @@ import * as testUtils from "../test-utils/test-utils";
2121
import { TestClient } from "../TestClient";
2222
import { logger } from "../../src/logger";
2323
import {
24+
IClaimOTKsResult,
2425
IContent,
26+
IDownloadKeyResult,
2527
IEvent,
26-
IClaimOTKsResult,
2728
IJoinedRoom,
29+
IndexedDBCryptoStore,
2830
ISyncResponse,
29-
IDownloadKeyResult,
31+
IUploadKeysRequest,
3032
MatrixEvent,
3133
MatrixEventEvent,
32-
IndexedDBCryptoStore,
3334
Room,
35+
RoomMember,
36+
RoomStateEvent,
3437
} from "../../src/matrix";
3538
import { IDeviceKeys } from "../../src/crypto/dehydration";
3639
import { DeviceInfo } from "../../src/crypto/deviceinfo";
@@ -327,7 +330,9 @@ describe("megolm", () => {
327330
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
328331
const event = room.getLiveTimeline().getEvents()[0];
329332
expect(event.isEncrypted()).toBe(true);
330-
const decryptedEvent = await testUtils.awaitDecryption(event);
333+
334+
// it probably won't be decrypted yet, because it takes a while to process the olm keys
335+
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
331336
expect(decryptedEvent.getContent().body).toEqual('42');
332337
});
333338

@@ -873,7 +878,12 @@ describe("megolm", () => {
873878

874879
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
875880
await room.decryptCriticalEvents();
876-
expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42');
881+
882+
// it probably won't be decrypted yet, because it takes a while to process the olm keys
883+
const decryptedEvent = await testUtils.awaitDecryption(
884+
room.getLiveTimeline().getEvents()[0], { waitOnDecryptionFailure: true },
885+
);
886+
expect(decryptedEvent.getContent().body).toEqual('42');
877887

878888
const exported = await aliceTestClient.client.exportRoomKeys();
879889

@@ -1012,7 +1022,9 @@ describe("megolm", () => {
10121022
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
10131023
const event = room.getLiveTimeline().getEvents()[0];
10141024
expect(event.isEncrypted()).toBe(true);
1015-
const decryptedEvent = await testUtils.awaitDecryption(event);
1025+
1026+
// it probably won't be decrypted yet, because it takes a while to process the olm keys
1027+
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
10161028
expect(decryptedEvent.getRoomId()).toEqual(ROOM_ID);
10171029
expect(decryptedEvent.getContent()).toEqual({});
10181030
expect(decryptedEvent.getClearContent()).toBeUndefined();
@@ -1364,4 +1376,90 @@ describe("megolm", () => {
13641376

13651377
await beccaTestClient.stop();
13661378
});
1379+
1380+
it("allows sending an encrypted event as soon as room state arrives", async () => {
1381+
/* Empirically, clients expect to be able to send encrypted events as soon as the
1382+
* RoomStateEvent.NewMember notification is emitted, so test that works correctly.
1383+
*/
1384+
const testRoomId = "!testRoom:id";
1385+
await aliceTestClient.start();
1386+
1387+
aliceTestClient.httpBackend.when("POST", "/keys/query")
1388+
.respond(200, function(_path, content: IUploadKeysRequest) {
1389+
return { device_keys: {} };
1390+
});
1391+
1392+
/* Alice makes the /createRoom call */
1393+
aliceTestClient.httpBackend.when("POST", "/createRoom")
1394+
.respond(200, { room_id: testRoomId });
1395+
await Promise.all([
1396+
aliceTestClient.client.createRoom({
1397+
initial_state: [{
1398+
type: 'm.room.encryption',
1399+
state_key: '',
1400+
content: { algorithm: 'm.megolm.v1.aes-sha2' },
1401+
}],
1402+
}),
1403+
aliceTestClient.httpBackend.flushAllExpected(),
1404+
]);
1405+
1406+
/* The sync arrives in two parts; first the m.room.create... */
1407+
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
1408+
rooms: { join: {
1409+
[testRoomId]: {
1410+
timeline: { events: [
1411+
{
1412+
type: 'm.room.create',
1413+
state_key: '',
1414+
event_id: "$create",
1415+
},
1416+
{
1417+
type: 'm.room.member',
1418+
state_key: aliceTestClient.getUserId(),
1419+
content: { membership: "join" },
1420+
event_id: "$alijoin",
1421+
},
1422+
] },
1423+
},
1424+
} },
1425+
});
1426+
await aliceTestClient.flushSync();
1427+
1428+
// ... and then the e2e event and an invite ...
1429+
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
1430+
rooms: { join: {
1431+
[testRoomId]: {
1432+
timeline: { events: [
1433+
{
1434+
type: 'm.room.encryption',
1435+
state_key: '',
1436+
content: { algorithm: 'm.megolm.v1.aes-sha2' },
1437+
event_id: "$e2e",
1438+
},
1439+
{
1440+
type: 'm.room.member',
1441+
state_key: "@other:user",
1442+
content: { membership: "invite" },
1443+
event_id: "$otherinvite",
1444+
},
1445+
] },
1446+
},
1447+
} },
1448+
});
1449+
1450+
// as soon as the roomMember arrives, try to send a message
1451+
aliceTestClient.client.on(RoomStateEvent.NewMember, (_e, _s, member: RoomMember) => {
1452+
if (member.userId == "@other:user") {
1453+
aliceTestClient.client.sendMessage(testRoomId, { msgtype: "m.text", body: "Hello, World" });
1454+
}
1455+
});
1456+
1457+
// flush the sync and wait for the /send/ request.
1458+
aliceTestClient.httpBackend.when("PUT", "/send/m.room.encrypted/")
1459+
.respond(200, (_path, _content) => ({ event_id: "asdfgh" }));
1460+
await Promise.all([
1461+
aliceTestClient.flushSync(),
1462+
aliceTestClient.httpBackend.flush("/send/m.room.encrypted/", 1),
1463+
]);
1464+
});
13671465
});

spec/integ/sliding-sync-sdk.spec.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,4 +928,90 @@ describe("SlidingSyncSdk", () => {
928928
expect(room.getMember(selfUserId)?.typing).toEqual(false);
929929
});
930930
});
931+
932+
describe("ExtensionReceipts", () => {
933+
let ext: Extension;
934+
935+
const generateReceiptResponse = (
936+
userId: string, roomId: string, eventId: string, recType: string, ts: number,
937+
) => {
938+
return {
939+
rooms: {
940+
[roomId]: {
941+
type: EventType.Receipt,
942+
content: {
943+
[eventId]: {
944+
[recType]: {
945+
[userId]: {
946+
ts: ts,
947+
},
948+
},
949+
},
950+
},
951+
},
952+
},
953+
};
954+
};
955+
956+
beforeAll(async () => {
957+
await setupClient();
958+
const hasSynced = sdk!.sync();
959+
await httpBackend!.flushAllExpected();
960+
await hasSynced;
961+
ext = findExtension("receipts");
962+
});
963+
964+
it("gets enabled on the initial request only", () => {
965+
expect(ext.onRequest(true)).toEqual({
966+
enabled: true,
967+
});
968+
expect(ext.onRequest(false)).toEqual(undefined);
969+
});
970+
971+
it("processes receipts", async () => {
972+
const roomId = "!room:id";
973+
const alice = "@alice:alice";
974+
const lastEvent = mkOwnEvent(EventType.RoomMessage, { body: "hello" });
975+
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
976+
name: "Room with receipts",
977+
required_state: [],
978+
timeline: [
979+
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
980+
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
981+
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
982+
{
983+
type: EventType.RoomMember,
984+
state_key: alice,
985+
content: { membership: "join" },
986+
sender: alice,
987+
origin_server_ts: Date.now(),
988+
event_id: "$alice",
989+
},
990+
lastEvent,
991+
],
992+
initial: true,
993+
});
994+
const room = client!.getRoom(roomId)!;
995+
expect(room).toBeDefined();
996+
expect(room.getReadReceiptForUserId(alice, true)).toBeNull();
997+
ext.onResponse(
998+
generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567),
999+
);
1000+
const receipt = room.getReadReceiptForUserId(alice);
1001+
expect(receipt).toBeDefined();
1002+
expect(receipt?.eventId).toEqual(lastEvent.event_id);
1003+
expect(receipt?.data.ts).toEqual(1234567);
1004+
expect(receipt?.data.thread_id).toBeFalsy();
1005+
});
1006+
1007+
it("gracefully handles missing rooms when receiving receipts", async () => {
1008+
const roomId = "!room:id";
1009+
const alice = "@alice:alice";
1010+
const eventId = "$something";
1011+
ext.onResponse(
1012+
generateReceiptResponse(alice, roomId, eventId, "m.read", 1234567),
1013+
);
1014+
// we expect it not to crash
1015+
});
1016+
});
9311017
});

spec/test-utils/test-utils.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -362,22 +362,28 @@ export class MockStorageApi {
362362
* @param {MatrixEvent} event
363363
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
364364
*/
365-
export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent> {
365+
export async function awaitDecryption(
366+
event: MatrixEvent, { waitOnDecryptionFailure = false } = {},
367+
): Promise<MatrixEvent> {
366368
// An event is not always decrypted ahead of time
367369
// getClearContent is a good signal to know whether an event has been decrypted
368370
// already
369371
if (event.getClearContent() !== null) {
370-
return event;
372+
if (waitOnDecryptionFailure && event.isDecryptionFailure()) {
373+
logger.log(`${Date.now()} event ${event.getId()} got decryption error; waiting`);
374+
} else {
375+
return event;
376+
}
371377
} else {
372-
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
378+
logger.log(`${Date.now()} event ${event.getId()} is not yet decrypted; waiting`);
379+
}
373380

374-
return new Promise((resolve) => {
375-
event.once(MatrixEventEvent.Decrypted, (ev) => {
376-
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
377-
resolve(ev);
378-
});
381+
return new Promise((resolve) => {
382+
event.once(MatrixEventEvent.Decrypted, (ev) => {
383+
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
384+
resolve(ev);
379385
});
380-
}
386+
});
381387
}
382388

383389
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));

spec/test-utils/webrtc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
431431
export class MockCallFeed {
432432
constructor(
433433
public userId: string,
434+
public deviceId: string | undefined,
434435
public stream: MockMediaStream,
435436
) {}
436437

0 commit comments

Comments
 (0)