Skip to content

Commit a246bac

Browse files
authored
matrix: Add room retention policy (#2040)
1 parent d00a7f3 commit a246bac

File tree

7 files changed

+191
-1
lines changed

7 files changed

+191
-1
lines changed

packages/matrix/docker/synapse/dev/homeserver.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ log_config: "/data/log.config"
2222
presence:
2323
enabled: false
2424

25+
retention:
26+
enabled: true
27+
2528
rc_messages_per_second: 10000
2629
rc_message_burst_count: 10000
2730
rc_registration:

packages/matrix/docker/synapse/index.ts

+39
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,45 @@ export async function getJoinedRooms(accessToken: string) {
429429
return joined_rooms;
430430
}
431431

432+
export async function getRoomStateEventType(
433+
accessToken: string,
434+
roomId: string,
435+
eventType: string,
436+
) {
437+
let response = await fetch(
438+
`http://localhost:${SYNAPSE_PORT}/_matrix/client/v3/rooms/${roomId}/state/${eventType}`,
439+
{
440+
headers: {
441+
Authorization: `Bearer ${accessToken}`,
442+
},
443+
},
444+
);
445+
return await response.json();
446+
}
447+
448+
export async function getRoomName(accessToken: string, roomId: string) {
449+
return await getRoomStateEventType(accessToken, roomId, 'm.room.name');
450+
}
451+
452+
export async function getRoomRetentionPolicy(
453+
accessToken: string,
454+
roomId: string,
455+
) {
456+
return await getRoomStateEventType(accessToken, roomId, 'm.room.retention');
457+
}
458+
459+
export async function getRoomMembers(roomId: string, accessToken: string) {
460+
let response = await fetch(
461+
`http://localhost:${SYNAPSE_PORT}/_matrix/client/v3/rooms/${roomId}/joined_members`,
462+
{
463+
headers: {
464+
Authorization: `Bearer ${accessToken}`,
465+
},
466+
},
467+
);
468+
return await response.json();
469+
}
470+
432471
export async function sync(accessToken: string) {
433472
let response = await fetch(
434473
`http://localhost:${SYNAPSE_PORT}/_matrix/client/v3/sync`,

packages/matrix/docker/synapse/test-without-registration-token/homeserver.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ database:
1919

2020
log_config: "/data/log.config"
2121

22+
retention:
23+
enabled: true
24+
2225
rc_messages_per_second: 10000
2326
rc_message_burst_count: 10000
2427
rc_registration:

packages/matrix/docker/synapse/test/homeserver.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ log_config: "/data/log.config"
2222
presence:
2323
enabled: false
2424

25+
retention:
26+
enabled: true
27+
2528
rc_messages_per_second: 10000
2629
rc_message_burst_count: 10000
2730
rc_registration:
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { expect, test } from '@playwright/test';
2+
import {
3+
synapseStart,
4+
synapseStop,
5+
type SynapseInstance,
6+
registerUser,
7+
getJoinedRooms,
8+
getRoomMembers,
9+
getRoomRetentionPolicy,
10+
} from '../docker/synapse';
11+
import { smtpStart, smtpStop } from '../docker/smtp4dev';
12+
import { login, registerRealmUsers, setupUserSubscribed } from '../helpers';
13+
14+
import {
15+
appURL,
16+
startServer as startRealmServer,
17+
type IsolatedRealmServer,
18+
} from '../helpers/isolated-realm-server';
19+
20+
test.describe('Auth rooms', () => {
21+
let synapse: SynapseInstance;
22+
let realmServer: IsolatedRealmServer;
23+
let user: { accessToken: string };
24+
25+
test.beforeEach(async () => {
26+
// synapse defaults to 30s for beforeEach to finish, we need a bit more time
27+
// to safely start the realm
28+
test.setTimeout(120_000);
29+
synapse = await synapseStart({
30+
template: 'test',
31+
});
32+
await smtpStart();
33+
34+
await registerRealmUsers(synapse);
35+
realmServer = await startRealmServer();
36+
37+
user = await registerUser(synapse, 'user1', 'pass');
38+
await setupUserSubscribed('@user1:localhost', realmServer);
39+
});
40+
41+
test.afterEach(async () => {
42+
await synapseStop(synapse.synapseId);
43+
await smtpStop();
44+
await realmServer.stop();
45+
});
46+
47+
test('auth rooms have a retention policy', async ({ page }) => {
48+
await login(page, 'user1', 'pass', { url: appURL });
49+
50+
let roomIds = await getJoinedRooms(user.accessToken);
51+
52+
let roomIdToMembers = new Map<string, any>();
53+
54+
for (let room of roomIds) {
55+
let members = await getRoomMembers(room, user.accessToken);
56+
roomIdToMembers.set(room, members);
57+
}
58+
59+
let realmUsers = ['@base_realm:localhost', '@test_realm:localhost'];
60+
61+
let realmRoomIds = roomIds.filter((room) =>
62+
realmUsers.some((user) => roomIdToMembers.get(room)?.joined[user]),
63+
);
64+
65+
expect(realmRoomIds.length).toBe(realmUsers.length);
66+
67+
for (let room of realmRoomIds) {
68+
let retentionPolicy = await getRoomRetentionPolicy(
69+
user.accessToken,
70+
room,
71+
);
72+
73+
expect(retentionPolicy).toMatchObject({
74+
max_lifetime: 60 * 60 * 1000,
75+
});
76+
}
77+
});
78+
});

packages/runtime-common/matrix-client.ts

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Sha256 } from '@aws-crypto/sha256-js';
22
import { uint8ArrayToHex } from './index';
3+
import { REALM_ROOM_RETENTION_POLICY_MAX_LIFETIME } from './realm';
4+
import { Deferred } from './deferred';
35

46
export interface MatrixAccess {
57
accessToken: string;
@@ -13,6 +15,7 @@ export class MatrixClient {
1315
private access: MatrixAccess | undefined;
1416
private password?: string;
1517
private seed?: string;
18+
private loggedIn = new Deferred<void>();
1619

1720
constructor({
1821
matrixURL,
@@ -44,6 +47,10 @@ export class MatrixClient {
4447
return this.access !== undefined;
4548
}
4649

50+
async waitForLogin() {
51+
return this.loggedIn.promise;
52+
}
53+
4754
private async request(
4855
path: string,
4956
method: 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'GET' = 'GET',
@@ -96,18 +103,21 @@ export class MatrixClient {
96103
let json = await response.json();
97104

98105
if (!response.ok) {
99-
throw new Error(
106+
let error = new Error(
100107
`Unable to login to matrix ${this.matrixURL.href} as user ${
101108
this.username
102109
}: status ${response.status} - ${JSON.stringify(json)}`,
103110
);
111+
this.loggedIn.reject(error);
112+
throw error;
104113
}
105114
let {
106115
access_token: accessToken,
107116
device_id: deviceId,
108117
user_id: userId,
109118
} = json;
110119
this.access = { accessToken, deviceId, userId };
120+
this.loggedIn.fulfill();
111121
}
112122

113123
async getJoinedRooms() {
@@ -146,9 +156,41 @@ export class MatrixClient {
146156
} - ${JSON.stringify(json)}`,
147157
);
148158
}
159+
160+
await this.setRoomRetentionPolicy(
161+
json.room_id,
162+
REALM_ROOM_RETENTION_POLICY_MAX_LIFETIME,
163+
);
164+
149165
return json.room_id;
150166
}
151167

168+
async setRoomRetentionPolicy(roomId: string, maxLifetimeMs: number) {
169+
try {
170+
let roomState = await this.request(
171+
`_matrix/client/v3/rooms/${roomId}/state`,
172+
);
173+
174+
let roomStateJson = await roomState.json();
175+
176+
let retentionState = roomStateJson.find(
177+
(event: any) => event.type === 'm.room.retention',
178+
);
179+
180+
let retentionStateKey = retentionState?.content.key ?? '';
181+
182+
await this.request(
183+
`_matrix/client/v3/rooms/${roomId}/state/m.room.retention/${retentionStateKey}`,
184+
'PUT',
185+
{
186+
body: JSON.stringify({ max_lifetime: maxLifetimeMs }),
187+
},
188+
);
189+
} catch (e) {
190+
console.error('error setting retention policy', e);
191+
}
192+
}
193+
152194
async setAccountData<T>(type: string, data: T) {
153195
let response = await this.request(
154196
`_matrix/client/v3/user/${encodeURIComponent(

packages/runtime-common/realm.ts

+22
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ import {
8181
Utils,
8282
} from './matrix-backend-authentication';
8383

84+
export const REALM_ROOM_RETENTION_POLICY_MAX_LIFETIME = 60 * 60 * 1000;
85+
8486
export interface RealmSession {
8587
canRead: boolean;
8688
canWrite: boolean;
@@ -330,6 +332,9 @@ export class Realm {
330332
),
331333
]);
332334

335+
// TODO: remove after running in all environments; CS-7875
336+
this.backfillRetentionPolicies();
337+
333338
let loader = new Loader(fetch, virtualNetwork.resolveImport);
334339
adapter.setLoader?.(loader);
335340

@@ -449,6 +454,23 @@ export class Realm {
449454
});
450455
}
451456

457+
// TODO: remove after running in all environments; CS-7875
458+
private async backfillRetentionPolicies() {
459+
try {
460+
await this.#matrixClient.waitForLogin();
461+
462+
let roomIds = (await this.#matrixClient.getJoinedRooms()).joined_rooms;
463+
for (let roomId of roomIds) {
464+
await this.#matrixClient.setRoomRetentionPolicy(
465+
roomId,
466+
REALM_ROOM_RETENTION_POLICY_MAX_LIFETIME,
467+
);
468+
}
469+
} catch (e) {
470+
console.error('backfillRetentionPolicies: error', e);
471+
}
472+
}
473+
452474
async indexing() {
453475
return this.#realmIndexUpdater.indexing();
454476
}

0 commit comments

Comments
 (0)