matrix-js-sdk 41.7.0 → 41.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/lib/@types/event.d.ts +6 -1
- package/lib/@types/event.d.ts.map +1 -1
- package/lib/@types/event.js +3 -0
- package/lib/@types/event.js.map +1 -1
- package/lib/@types/retention.d.ts +15 -0
- package/lib/@types/retention.d.ts.map +1 -0
- package/lib/@types/retention.js +13 -0
- package/lib/@types/retention.js.map +1 -0
- package/lib/capabilityPoller.d.ts +38 -0
- package/lib/capabilityPoller.d.ts.map +1 -0
- package/lib/capabilityPoller.js +96 -0
- package/lib/capabilityPoller.js.map +1 -0
- package/lib/client.d.ts +7 -0
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +15 -5
- package/lib/client.js.map +1 -1
- package/lib/matrix.d.ts +1 -0
- package/lib/matrix.d.ts.map +1 -1
- package/lib/matrix.js +1 -0
- package/lib/matrix.js.map +1 -1
- package/lib/matrixrtc/MembershipManager.d.ts +2 -0
- package/lib/matrixrtc/MembershipManager.d.ts.map +1 -1
- package/lib/matrixrtc/MembershipManager.js +2 -0
- package/lib/matrixrtc/MembershipManager.js.map +1 -1
- package/lib/matrixrtc/MembershipManagerActionScheduler.d.ts.map +1 -1
- package/lib/matrixrtc/MembershipManagerActionScheduler.js +4 -1
- package/lib/matrixrtc/MembershipManagerActionScheduler.js.map +1 -1
- package/lib/models/room-retention.d.ts +28 -0
- package/lib/models/room-retention.d.ts.map +1 -0
- package/lib/models/room-retention.js +201 -0
- package/lib/models/room-retention.js.map +1 -0
- package/lib/models/room.d.ts +2 -1
- package/lib/models/room.d.ts.map +1 -1
- package/lib/models/room.js +29 -3
- package/lib/models/room.js.map +1 -1
- package/lib/retentionPolicy.d.ts +33 -0
- package/lib/retentionPolicy.d.ts.map +1 -0
- package/lib/retentionPolicy.js +42 -0
- package/lib/retentionPolicy.js.map +1 -0
- package/lib/rust-crypto/backup.d.ts +7 -2
- package/lib/rust-crypto/backup.d.ts.map +1 -1
- package/lib/rust-crypto/backup.js +13 -4
- package/lib/rust-crypto/backup.js.map +1 -1
- package/lib/serverCapabilities.d.ts +4 -23
- package/lib/serverCapabilities.d.ts.map +1 -1
- package/lib/serverCapabilities.js +8 -61
- package/lib/serverCapabilities.js.map +1 -1
- package/lib/store/index.d.ts +7 -0
- package/lib/store/index.d.ts.map +1 -1
- package/lib/store/index.js.map +1 -1
- package/lib/store/indexeddb-local-backend.d.ts +1 -0
- package/lib/store/indexeddb-local-backend.d.ts.map +1 -1
- package/lib/store/indexeddb-local-backend.js +15 -3
- package/lib/store/indexeddb-local-backend.js.map +1 -1
- package/lib/store/indexeddb-remote-backend.d.ts +3 -1
- package/lib/store/indexeddb-remote-backend.d.ts.map +1 -1
- package/lib/store/indexeddb-remote-backend.js +29 -8
- package/lib/store/indexeddb-remote-backend.js.map +1 -1
- package/lib/store/indexeddb.d.ts +7 -5
- package/lib/store/indexeddb.d.ts.map +1 -1
- package/lib/store/indexeddb.js +20 -10
- package/lib/store/indexeddb.js.map +1 -1
- package/lib/store/memory.d.ts +1 -0
- package/lib/store/memory.d.ts.map +1 -1
- package/lib/store/memory.js +3 -0
- package/lib/store/memory.js.map +1 -1
- package/lib/store/stub.d.ts +1 -0
- package/lib/store/stub.d.ts.map +1 -1
- package/lib/store/stub.js +3 -0
- package/lib/store/stub.js.map +1 -1
- package/lib/sync-accumulator.d.ts +1 -0
- package/lib/sync-accumulator.d.ts.map +1 -1
- package/lib/sync-accumulator.js +4 -0
- package/lib/sync-accumulator.js.map +1 -1
- package/package.json +3 -3
- package/src/@types/event.ts +9 -0
- package/src/@types/retention.ts +16 -0
- package/src/capabilityPoller.ts +102 -0
- package/src/client.ts +18 -4
- package/src/matrix.ts +1 -0
- package/src/matrixrtc/MembershipManager.ts +2 -0
- package/src/matrixrtc/MembershipManagerActionScheduler.ts +4 -1
- package/src/models/room-retention.ts +230 -0
- package/src/models/room.ts +17 -1
- package/src/retentionPolicy.ts +65 -0
- package/src/rust-crypto/backup.ts +15 -4
- package/src/serverCapabilities.ts +8 -67
- package/src/store/index.ts +8 -0
- package/src/store/indexeddb-local-backend.ts +10 -0
- package/src/store/indexeddb-remote-backend.ts +24 -5
- package/src/store/indexeddb.ts +20 -8
- package/src/store/memory.ts +4 -0
- package/src/store/stub.ts +3 -0
- package/src/sync-accumulator.ts +9 -0
package/src/client.ts
CHANGED
|
@@ -249,6 +249,7 @@ import {
|
|
|
249
249
|
import { type EmptyObject } from "./@types/common.ts";
|
|
250
250
|
import { UnsupportedDelayedEventsEndpointError, UnsupportedStickyEventsEndpointError } from "./errors.ts";
|
|
251
251
|
import { type Transport } from "./matrixrtc/index.ts";
|
|
252
|
+
import { RetentionPolicyService } from "./retentionPolicy.ts";
|
|
252
253
|
|
|
253
254
|
export type Store = IStore;
|
|
254
255
|
|
|
@@ -462,6 +463,11 @@ export interface ICreateClientOpts {
|
|
|
462
463
|
* Defaults to the built-in global logger; see {@link DebugLogger} for an alternative.
|
|
463
464
|
*/
|
|
464
465
|
logger?: Logger;
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Locally remove events past the server global or per room retention setting.
|
|
469
|
+
*/
|
|
470
|
+
unstableMSC1763Retention?: boolean;
|
|
465
471
|
}
|
|
466
472
|
|
|
467
473
|
export interface IMatrixClientCreateOpts extends ICreateClientOpts {
|
|
@@ -1327,6 +1333,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
1327
1333
|
public readonly matrixRTC: MatrixRTCSessionManager;
|
|
1328
1334
|
|
|
1329
1335
|
private serverCapabilitiesService: ServerCapabilities;
|
|
1336
|
+
public readonly retentionPolicyService: RetentionPolicyService;
|
|
1337
|
+
// eslint-disable-next-line
|
|
1338
|
+
public readonly _unstable_shouldApplyMessageRetention: boolean;
|
|
1330
1339
|
|
|
1331
1340
|
public constructor(opts: IMatrixClientCreateOpts) {
|
|
1332
1341
|
super();
|
|
@@ -1405,6 +1414,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
1405
1414
|
this.matrixRTC = new MatrixRTCSessionManager(this.logger, this);
|
|
1406
1415
|
|
|
1407
1416
|
this.serverCapabilitiesService = new ServerCapabilities(this.logger, this.http);
|
|
1417
|
+
this.retentionPolicyService = new RetentionPolicyService(this.logger, this.http);
|
|
1418
|
+
this._unstable_shouldApplyMessageRetention = opts.unstableMSC1763Retention ?? false;
|
|
1408
1419
|
|
|
1409
1420
|
this.on(ClientEvent.Sync, this.fixupRoomNotifications);
|
|
1410
1421
|
|
|
@@ -1530,6 +1541,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
1530
1541
|
|
|
1531
1542
|
this.toDeviceMessageQueue.start();
|
|
1532
1543
|
this.serverCapabilitiesService.start();
|
|
1544
|
+
if (this._unstable_shouldApplyMessageRetention) {
|
|
1545
|
+
this.retentionPolicyService?.start();
|
|
1546
|
+
}
|
|
1533
1547
|
}
|
|
1534
1548
|
|
|
1535
1549
|
/**
|
|
@@ -1936,9 +1950,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
1936
1950
|
* @returns Promise resolving with The capabilities of the homeserver
|
|
1937
1951
|
*/
|
|
1938
1952
|
public async getCapabilities(): Promise<Capabilities> {
|
|
1939
|
-
const caps = this.serverCapabilitiesService.
|
|
1953
|
+
const caps = this.serverCapabilitiesService.getCached();
|
|
1940
1954
|
if (caps) return caps;
|
|
1941
|
-
return this.serverCapabilitiesService.
|
|
1955
|
+
return this.serverCapabilitiesService.fetch();
|
|
1942
1956
|
}
|
|
1943
1957
|
|
|
1944
1958
|
/**
|
|
@@ -1948,7 +1962,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
1948
1962
|
* @returns The capabilities of the homeserver
|
|
1949
1963
|
*/
|
|
1950
1964
|
public getCachedCapabilities(): Capabilities | undefined {
|
|
1951
|
-
return this.serverCapabilitiesService.
|
|
1965
|
+
return this.serverCapabilitiesService.getCached();
|
|
1952
1966
|
}
|
|
1953
1967
|
|
|
1954
1968
|
/**
|
|
@@ -1958,7 +1972,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
1958
1972
|
* @returns A promise which resolves to the capabilities of the homeserver
|
|
1959
1973
|
*/
|
|
1960
1974
|
public fetchCapabilities(): Promise<Capabilities> {
|
|
1961
|
-
return this.serverCapabilitiesService.
|
|
1975
|
+
return this.serverCapabilitiesService.fetch();
|
|
1962
1976
|
}
|
|
1963
1977
|
|
|
1964
1978
|
/**
|
package/src/matrix.ts
CHANGED
|
@@ -71,6 +71,7 @@ export * from "./@types/location.ts";
|
|
|
71
71
|
export * from "./@types/threepids.ts";
|
|
72
72
|
export * from "./@types/auth.ts";
|
|
73
73
|
export * from "./@types/polls.ts";
|
|
74
|
+
export * from "./@types/retention.ts";
|
|
74
75
|
export type * from "./@types/local_notifications.ts";
|
|
75
76
|
export type * from "./@types/registration.ts";
|
|
76
77
|
export * from "./@types/read_receipts.ts";
|
|
@@ -207,6 +207,8 @@ export class MembershipManager
|
|
|
207
207
|
* transport selection will be used instead.
|
|
208
208
|
* @param onError This will be called once the membership manager encounters an unrecoverable error.
|
|
209
209
|
* This should bubble up the the frontend to communicate that the call does not work in the current environment.
|
|
210
|
+
* The original underlying error is preserved as `error.cause`, so consumers can `instanceof`-check the typed
|
|
211
|
+
* cause.
|
|
210
212
|
*/
|
|
211
213
|
public join(fociPreferred: Transport[], multiSfuFocus?: Transport, onError?: (error: unknown) => void): void {
|
|
212
214
|
if (this.scheduler.running) {
|
|
@@ -103,7 +103,10 @@ export class ActionScheduler {
|
|
|
103
103
|
// `this.wakeup` can also be called and sets the `wakeupUpdate` object while we are in the handler.
|
|
104
104
|
handlerResult = await this.membershipLoopHandler(nextAction.type as MembershipActionType);
|
|
105
105
|
} catch (e) {
|
|
106
|
-
|
|
106
|
+
// Preserve the original error as `cause`.
|
|
107
|
+
throw new Error(`The MembershipManager shut down because of the end condition: ${e}`, {
|
|
108
|
+
cause: e,
|
|
109
|
+
});
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
// remove the processed action only after we are done processing
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 Element Creations Ltd.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { MatrixEvent } from "../models/event.ts";
|
|
18
|
+
import { RoomEvent, type Room } from "./room.ts";
|
|
19
|
+
import { RoomStateEvent, type RoomState } from "./room-state.ts";
|
|
20
|
+
import { type Logger, logger as rootLogger } from "../logger.ts";
|
|
21
|
+
import { EventType } from "../@types/event.ts";
|
|
22
|
+
import type { RetentionPolicyService } from "../retentionPolicy.ts";
|
|
23
|
+
import type { IStore } from "../store/index.ts";
|
|
24
|
+
import { ROOM_RETENTION_TYPE, type RoomRetentionContent } from "../@types/retention.ts";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Applies https://github.com/matrix-org/matrix-spec-proposals/pull/1763 by checking the current
|
|
28
|
+
* state of the room and applying the *latest* retention policy to all preceeding events.
|
|
29
|
+
* Currently does not:
|
|
30
|
+
* - Apply global server policies (requires Synapse work)
|
|
31
|
+
* - React at all to `min_retention` configs.
|
|
32
|
+
* - Loop
|
|
33
|
+
*/
|
|
34
|
+
export class RoomRetentionPolicy {
|
|
35
|
+
// minRetention is not implemented.
|
|
36
|
+
private maxRetention: number | null = null;
|
|
37
|
+
private retentionTimeout?: ReturnType<typeof setTimeout>;
|
|
38
|
+
private readonly logger: Logger;
|
|
39
|
+
|
|
40
|
+
public constructor(
|
|
41
|
+
private readonly room: Room,
|
|
42
|
+
private readonly retentionService: RetentionPolicyService,
|
|
43
|
+
private readonly store: IStore,
|
|
44
|
+
) {
|
|
45
|
+
this.logger = rootLogger.getChild(`RetentionPolicy ${room.roomId}`);
|
|
46
|
+
|
|
47
|
+
// Recalculate on room state update.
|
|
48
|
+
room.on(RoomStateEvent.Events, (event) => {
|
|
49
|
+
if (
|
|
50
|
+
event.getStateKey() !== "" ||
|
|
51
|
+
(event.getType() !== "org.matrix.msc1763.retention" && event.getType() !== "m.room.retention")
|
|
52
|
+
) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
void this.handleRetentionUpdate();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Recalculate our retention whenever the global policy changes.
|
|
59
|
+
this.retentionService.on("update", () => {
|
|
60
|
+
this.logger.info("Got global retention policy update!");
|
|
61
|
+
void this.handleRetentionUpdate();
|
|
62
|
+
});
|
|
63
|
+
// Do an initial check on construction.
|
|
64
|
+
void this.handleRetentionUpdate();
|
|
65
|
+
|
|
66
|
+
// Only bind RoomEvent.Timeline once we know we have a retention policy.
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public shouldEventBeRetained(ev: MatrixEvent): boolean {
|
|
70
|
+
if (!this.maxRetention) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
const expireBefore = Date.now() - this.maxRetention;
|
|
74
|
+
return ev.getTs() > expireBefore;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private readonly handleRetentionUpdate = async (): Promise<void> => {
|
|
78
|
+
// First store if we currently have a retention period.
|
|
79
|
+
const hadRetentionPeriod = this.maxRetention !== null;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await this.recalculateRetention(this.room.currentState);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
this.logger.warn("Failed to recalculate retention policy", err);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Bind/unbind the Timeline event handler based on whether we should be running retention.
|
|
89
|
+
if (hadRetentionPeriod !== (this.maxRetention !== null)) {
|
|
90
|
+
this.logger.info("retention recalculated to be", this.maxRetention);
|
|
91
|
+
// We've changed
|
|
92
|
+
if (this.maxRetention) {
|
|
93
|
+
this.room.on(RoomEvent.Timeline, this.timelineUpdated);
|
|
94
|
+
} else {
|
|
95
|
+
this.room.off(RoomEvent.Timeline, this.timelineUpdated);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
private readonly recalculateRetention = async (roomState: RoomState): Promise<void> => {
|
|
101
|
+
const unstableEvent = roomState.getStateEvents(ROOM_RETENTION_TYPE.name).find((e) => e.getStateKey() === "");
|
|
102
|
+
const stableEvent = roomState.getStateEvents(ROOM_RETENTION_TYPE.altName).find((e) => e.getStateKey() === "");
|
|
103
|
+
|
|
104
|
+
const serverPolicy = this.retentionService.getCached();
|
|
105
|
+
|
|
106
|
+
const serverRoomPolicy = serverPolicy?.policies?.[this.room.roomId];
|
|
107
|
+
const roomStatePolicy =
|
|
108
|
+
unstableEvent?.getContent<RoomRetentionContent>() ?? stableEvent?.getContent<RoomRetentionContent>();
|
|
109
|
+
|
|
110
|
+
const content =
|
|
111
|
+
// * if the homeserver defines a specific retention policy for this room, then use this policy as the effective retention policy of the room.
|
|
112
|
+
serverRoomPolicy ??
|
|
113
|
+
// * otherwise, if the state of the room includes a `m.room.retention` event with an empty state key:
|
|
114
|
+
roomStatePolicy ??
|
|
115
|
+
// * otherwise, if the state of the room does not include a m.room.retention event with an empty state key:
|
|
116
|
+
serverPolicy?.policies?.["*"];
|
|
117
|
+
|
|
118
|
+
if (!content) {
|
|
119
|
+
// * otherwise, don't apply a retention policy in this room.
|
|
120
|
+
this.maxRetention = null;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const validatePolicy = !serverRoomPolicy && roomStatePolicy;
|
|
125
|
+
|
|
126
|
+
// Parse it
|
|
127
|
+
let { max_lifetime: maxLifetime } = content;
|
|
128
|
+
|
|
129
|
+
if (!maxLifetime && validatePolicy) {
|
|
130
|
+
// if there is no value specified in the room's state, use the limit's min value for the effective retention policy of the room (which can be null or absent).
|
|
131
|
+
maxLifetime = serverPolicy?.limits?.max_lifetime?.min;
|
|
132
|
+
if (!maxLifetime) {
|
|
133
|
+
this.maxRetention = null;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (typeof maxLifetime !== "number") {
|
|
139
|
+
throw Error(`max_lifetime must be a number, got "${maxLifetime}"`);
|
|
140
|
+
}
|
|
141
|
+
if (maxLifetime < 0 || !Number.isInteger(maxLifetime)) {
|
|
142
|
+
throw Error(`max_lifetime must be >= 0, got ${maxLifetime}`);
|
|
143
|
+
}
|
|
144
|
+
this.maxRetention = maxLifetime;
|
|
145
|
+
|
|
146
|
+
if (validatePolicy && serverPolicy?.limits?.max_lifetime) {
|
|
147
|
+
// We're using room state so we need to validate this policy matches the server.
|
|
148
|
+
/**
|
|
149
|
+
if the value specified in the room's state complies with the limit, use this value for the effective retention policy of the room.
|
|
150
|
+
if the value specified in the room's state is lower than the limit's min value, use the min value for the effective retention policy of the room.
|
|
151
|
+
if the value specified in the room's state is greater than the limit's max value, use the max value for the effective retention policy of the room.
|
|
152
|
+
*/
|
|
153
|
+
this.maxRetention = Math.max(this.maxRetention, serverPolicy.limits.max_lifetime.min ?? 0);
|
|
154
|
+
this.maxRetention = Math.min(
|
|
155
|
+
this.maxRetention,
|
|
156
|
+
serverPolicy.limits.max_lifetime.max ?? Number.MAX_SAFE_INTEGER,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.processTimeline();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
private readonly timelineUpdated = (): void => {
|
|
164
|
+
this.logger.info("timelineUpdated");
|
|
165
|
+
this.scheduleTimelineCheck(200);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
private readonly scheduleTimelineCheck = (nextTs: number): void => {
|
|
169
|
+
if (this.retentionTimeout) {
|
|
170
|
+
clearTimeout(this.retentionTimeout);
|
|
171
|
+
}
|
|
172
|
+
this.retentionTimeout = setTimeout(() => {
|
|
173
|
+
this.processTimeline();
|
|
174
|
+
this.retentionTimeout = undefined;
|
|
175
|
+
}, nextTs);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
private processTimeline(): void {
|
|
179
|
+
if (!this.maxRetention) {
|
|
180
|
+
return; // No policy, skip.
|
|
181
|
+
}
|
|
182
|
+
this.logger.info(`Running processTimeline`);
|
|
183
|
+
const expireBefore = Date.now() - this.maxRetention;
|
|
184
|
+
const events = this.room
|
|
185
|
+
.getLiveTimeline()
|
|
186
|
+
.getEvents()
|
|
187
|
+
.filter((ev) => ev.getStateKey() === undefined && !ev.isRedacted() && ev.getType() !== "m.room.redaction");
|
|
188
|
+
|
|
189
|
+
const expiredEvents = events.filter((ev) => ev.getTs() < expireBefore);
|
|
190
|
+
const [earliestExpiringEvent] = events
|
|
191
|
+
.filter((ev) => ev.getTs() >= expireBefore)
|
|
192
|
+
.sort((a, b) => a.getTs() - b.getTs());
|
|
193
|
+
|
|
194
|
+
if (earliestExpiringEvent) {
|
|
195
|
+
const nextTs = earliestExpiringEvent.getTs() + this.maxRetention - Date.now();
|
|
196
|
+
this.logger.info(`Next expiry scheduled for ${nextTs}`);
|
|
197
|
+
this.scheduleTimelineCheck(nextTs);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (expiredEvents.length === 0) {
|
|
201
|
+
this.logger.info("Found no expired events");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.logger.info(`Found ${expiredEvents.length} expired events`);
|
|
205
|
+
for (const event of expiredEvents) {
|
|
206
|
+
this.room.tryApplyRedaction(
|
|
207
|
+
new MatrixEvent({
|
|
208
|
+
type: EventType.RoomRedaction,
|
|
209
|
+
sender: event.getSender(),
|
|
210
|
+
event_id: "$synthetic_redaction",
|
|
211
|
+
room_id: this.room.roomId,
|
|
212
|
+
redacts: event.getId(),
|
|
213
|
+
content: {
|
|
214
|
+
reason: "Retention policy",
|
|
215
|
+
},
|
|
216
|
+
origin_server_ts: Date.now(),
|
|
217
|
+
unsigned: {},
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
void this.store
|
|
222
|
+
.removeEventsFromRoom(
|
|
223
|
+
this.room.roomId,
|
|
224
|
+
expiredEvents.map((e) => e.getId()!),
|
|
225
|
+
)
|
|
226
|
+
.catch((ex) => {
|
|
227
|
+
this.logger.warn(`Failed to remove events from store`, ex);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
package/src/models/room.ts
CHANGED
|
@@ -78,6 +78,7 @@ import { KnownMembership, type Membership } from "../@types/membership.ts";
|
|
|
78
78
|
import { type Capabilities, type IRoomVersionsCapability, RoomVersionStability } from "../serverCapabilities.ts";
|
|
79
79
|
import { type MSC4186Hero } from "../sliding-sync.ts";
|
|
80
80
|
import { RoomStickyEventsStore, RoomStickyEventsEvent, type RoomStickyEventsMap } from "./room-sticky-events.ts";
|
|
81
|
+
import { RoomRetentionPolicy } from "./room-retention.ts";
|
|
81
82
|
|
|
82
83
|
// These constants are used as sane defaults when the homeserver doesn't support
|
|
83
84
|
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
|
@@ -454,6 +455,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|
|
454
455
|
*/
|
|
455
456
|
private stickyEvents = new RoomStickyEventsStore();
|
|
456
457
|
|
|
458
|
+
private readonly retention?: RoomRetentionPolicy;
|
|
459
|
+
|
|
457
460
|
/**
|
|
458
461
|
* Construct a new Room.
|
|
459
462
|
*
|
|
@@ -530,6 +533,9 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|
|
530
533
|
} else {
|
|
531
534
|
this.membersPromise = undefined;
|
|
532
535
|
}
|
|
536
|
+
if (this.client?._unstable_shouldApplyMessageRetention) {
|
|
537
|
+
this.retention = new RoomRetentionPolicy(this, client.retentionPolicyService, client.store);
|
|
538
|
+
}
|
|
533
539
|
}
|
|
534
540
|
|
|
535
541
|
private threadTimelineSetsPromise: Promise<[EventTimelineSet, EventTimelineSet]> | null = null;
|
|
@@ -1864,6 +1870,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|
|
1864
1870
|
timeline: EventTimeline,
|
|
1865
1871
|
paginationToken?: string,
|
|
1866
1872
|
): void {
|
|
1873
|
+
// ALWAYS filter out any events that are past retention
|
|
1874
|
+
events = events.filter((e) => this.retention?.shouldEventBeRetained(e) ?? true);
|
|
1867
1875
|
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, addToState, timeline, paginationToken);
|
|
1868
1876
|
}
|
|
1869
1877
|
|
|
@@ -2466,6 +2474,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|
|
2466
2474
|
* Adds events to a thread's timeline. Will fire "Thread.update"
|
|
2467
2475
|
*/
|
|
2468
2476
|
public processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
|
2477
|
+
// ALWAYS filter out any events that are past retention
|
|
2478
|
+
events = events.filter((e) => this.retention?.shouldEventBeRetained(e) ?? true);
|
|
2469
2479
|
events.forEach(this.tryApplyRedaction);
|
|
2470
2480
|
|
|
2471
2481
|
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
|
|
@@ -2620,7 +2630,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|
|
2620
2630
|
}
|
|
2621
2631
|
}
|
|
2622
2632
|
|
|
2623
|
-
|
|
2633
|
+
public readonly tryApplyRedaction = (event: MatrixEvent): void => {
|
|
2624
2634
|
// FIXME: apply redactions to notification list
|
|
2625
2635
|
|
|
2626
2636
|
// NB: We continue to add the redaction event to the timeline at the
|
|
@@ -3082,6 +3092,9 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|
|
3082
3092
|
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
|
3083
3093
|
}
|
|
3084
3094
|
|
|
3095
|
+
// ALWAYS filter out any events that are past retention
|
|
3096
|
+
events = events.filter((e) => this.retention?.shouldEventBeRetained(e) ?? true);
|
|
3097
|
+
|
|
3085
3098
|
// sanity check that the live timeline is still live
|
|
3086
3099
|
this.assertTimelineSetsAreLive();
|
|
3087
3100
|
|
|
@@ -3476,6 +3489,9 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|
|
3476
3489
|
*/
|
|
3477
3490
|
// eslint-disable-next-line
|
|
3478
3491
|
public _unstable_addStickyEvents(events: MatrixEvent[]): ReturnType<RoomStickyEventsStore["addStickyEvents"]> {
|
|
3492
|
+
// ALWAYS filter out any events that are past retention
|
|
3493
|
+
events = events.filter((e) => this.retention?.shouldEventBeRetained(e) ?? true);
|
|
3494
|
+
|
|
3479
3495
|
return this.stickyEvents.addStickyEvents(events);
|
|
3480
3496
|
}
|
|
3481
3497
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024 The Matrix.org Foundation C.I.C.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { CapabilityPoller } from "./capabilityPoller.ts";
|
|
18
|
+
import { ClientPrefix, type IHttpOpts, type MatrixHttpApi, Method } from "./http-api/index.ts";
|
|
19
|
+
import type { Logger } from "./logger.ts";
|
|
20
|
+
|
|
21
|
+
export interface RetentionConfigurationResponse {
|
|
22
|
+
policies?: Record<
|
|
23
|
+
string,
|
|
24
|
+
{
|
|
25
|
+
max_lifetime?: number;
|
|
26
|
+
min_lifetime?: number;
|
|
27
|
+
}
|
|
28
|
+
>;
|
|
29
|
+
limits?: {
|
|
30
|
+
min_lifetime?: {
|
|
31
|
+
min?: number;
|
|
32
|
+
max?: number;
|
|
33
|
+
};
|
|
34
|
+
max_lifetime?: {
|
|
35
|
+
min?: number;
|
|
36
|
+
max?: number;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Manages storing and periodically refreshing the server capabilities.
|
|
43
|
+
*/
|
|
44
|
+
export class RetentionPolicyService extends CapabilityPoller<RetentionConfigurationResponse> {
|
|
45
|
+
public constructor(logger: Logger, http: MatrixHttpApi<IHttpOpts & { onlyData: true }>) {
|
|
46
|
+
super(logger, http, "retention policy");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fetches the latest server capabilities from the homeserver and returns them, or rejects
|
|
50
|
+
* on failure.
|
|
51
|
+
*/
|
|
52
|
+
public fetch = async (): Promise<RetentionConfigurationResponse> => {
|
|
53
|
+
const resp = await this.http.authedRequest<RetentionConfigurationResponse>(
|
|
54
|
+
Method.Get,
|
|
55
|
+
"/retention/configuration",
|
|
56
|
+
undefined,
|
|
57
|
+
undefined,
|
|
58
|
+
{
|
|
59
|
+
prefix: `${ClientPrefix.Unstable}/org.matrix.msc1763`,
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
this.cached = resp;
|
|
63
|
+
return this.cached;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -80,13 +80,17 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
|
|
80
80
|
/** whether {@link backupKeysLoop} is currently running */
|
|
81
81
|
private backupKeysLoopRunning = false;
|
|
82
82
|
|
|
83
|
+
/** The logger to use */
|
|
84
|
+
private readonly logger: Logger;
|
|
85
|
+
|
|
83
86
|
public constructor(
|
|
84
|
-
|
|
87
|
+
logger: Logger,
|
|
85
88
|
private readonly olmMachine: OlmMachine,
|
|
86
89
|
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
|
87
90
|
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
|
88
91
|
) {
|
|
89
92
|
super();
|
|
93
|
+
this.logger = logger.getChild("[RustBackupManager]");
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
/**
|
|
@@ -142,7 +146,8 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
|
|
142
146
|
/**
|
|
143
147
|
* Re-check the key backup and enable/disable it as appropriate.
|
|
144
148
|
*
|
|
145
|
-
* @param force - whether we should force a re-check even if one has already happened.
|
|
149
|
+
* @param force - whether we should force a re-check even if one has already happened. If this is
|
|
150
|
+
* `false`, and we have already done a check, `null` is returned rather than the actual info on the key backup.
|
|
146
151
|
*/
|
|
147
152
|
public checkKeyBackupAndEnable(force: boolean): Promise<KeyBackupCheck | null> {
|
|
148
153
|
if (!force && this.checkedForBackup) {
|
|
@@ -161,6 +166,9 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
|
|
161
166
|
/**
|
|
162
167
|
* Handles a backup secret received event and store it if it matches the current backup version.
|
|
163
168
|
*
|
|
169
|
+
* Also enables key backup upload if it was not previously enabled, and the encryption key matches the received
|
|
170
|
+
* decryption key.
|
|
171
|
+
*
|
|
164
172
|
* @param secret - The secret as received from a `m.secret.send` or `io.element.msc4385.secret.push` event for secret `m.megolm_backup.v1`.
|
|
165
173
|
* @returns true if the secret is valid and has been stored, false otherwise.
|
|
166
174
|
*/
|
|
@@ -206,11 +214,14 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
|
|
206
214
|
`handleBackupSecretReceived: Valid decryption key for the current server-side backup version (${latestBackupInfo.version}) received`,
|
|
207
215
|
);
|
|
208
216
|
await this.saveBackupDecryptionKey(backupDecryptionKey, latestBackupInfo.version);
|
|
209
|
-
|
|
210
|
-
//
|
|
217
|
+
|
|
218
|
+
// Check if backup upload should be enabled (e.g. the encryption key matches the decryption key),
|
|
219
|
+
// and enable it if so.
|
|
211
220
|
if (this.keyBackupCheckInProgress) {
|
|
221
|
+
this.logger.debug("handleBackupSecretReceived: waiting for ongoing keybackup check to complete");
|
|
212
222
|
await this.keyBackupCheckInProgress;
|
|
213
223
|
}
|
|
224
|
+
this.logger.debug("handleBackupSecretReceived: checking if we can enable keybackup upload");
|
|
214
225
|
this.keyBackupCheckInProgress = this.doCheckKeyBackup(latestBackupInfo).finally(() => {
|
|
215
226
|
this.keyBackupCheckInProgress = null;
|
|
216
227
|
});
|
|
@@ -14,15 +14,9 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { CapabilityPoller } from "./capabilityPoller.ts";
|
|
17
18
|
import { type IHttpOpts, type MatrixHttpApi, Method } from "./http-api/index.ts";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
// How often we update the server capabilities.
|
|
21
|
-
// 6 hours - an arbitrary value, but they should change very infrequently.
|
|
22
|
-
const CAPABILITIES_CACHE_MS = 6 * 60 * 60 * 1000;
|
|
23
|
-
|
|
24
|
-
// How long we want before retrying if we couldn't fetch
|
|
25
|
-
const CAPABILITIES_RETRY_MS = 30 * 1000;
|
|
19
|
+
import type { Logger } from "./logger.ts";
|
|
26
20
|
|
|
27
21
|
export interface ICapability {
|
|
28
22
|
enabled: boolean;
|
|
@@ -77,70 +71,17 @@ type CapabilitiesResponse = {
|
|
|
77
71
|
/**
|
|
78
72
|
* Manages storing and periodically refreshing the server capabilities.
|
|
79
73
|
*/
|
|
80
|
-
export class ServerCapabilities {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
private refreshTimeout?: ReturnType<typeof setInterval>;
|
|
84
|
-
|
|
85
|
-
public constructor(
|
|
86
|
-
private readonly logger: Logger,
|
|
87
|
-
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
|
88
|
-
) {}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Starts periodically fetching the server capabilities.
|
|
92
|
-
*/
|
|
93
|
-
public start(): void {
|
|
94
|
-
this.poll().then();
|
|
74
|
+
export class ServerCapabilities extends CapabilityPoller<Capabilities> {
|
|
75
|
+
public constructor(logger: Logger, http: MatrixHttpApi<IHttpOpts & { onlyData: true }>) {
|
|
76
|
+
super(logger, http, "server capabilities");
|
|
95
77
|
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Stops the service
|
|
99
|
-
*/
|
|
100
|
-
public stop(): void {
|
|
101
|
-
this.clearTimeouts();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Returns the cached capabilities, or undefined if none are cached.
|
|
106
|
-
* @returns the current capabilities, if any.
|
|
107
|
-
*/
|
|
108
|
-
public getCachedCapabilities(): Capabilities | undefined {
|
|
109
|
-
return this.capabilities;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
78
|
/**
|
|
113
79
|
* Fetches the latest server capabilities from the homeserver and returns them, or rejects
|
|
114
80
|
* on failure.
|
|
115
81
|
*/
|
|
116
|
-
public
|
|
82
|
+
public fetch = async (): Promise<Capabilities> => {
|
|
117
83
|
const resp = await this.http.authedRequest<CapabilitiesResponse>(Method.Get, "/capabilities");
|
|
118
|
-
this.
|
|
119
|
-
return this.
|
|
84
|
+
this.cached = resp["capabilities"];
|
|
85
|
+
return this.cached;
|
|
120
86
|
};
|
|
121
|
-
|
|
122
|
-
private poll = async (): Promise<void> => {
|
|
123
|
-
try {
|
|
124
|
-
await this.fetchCapabilities();
|
|
125
|
-
this.clearTimeouts();
|
|
126
|
-
this.refreshTimeout = setTimeout(this.poll, CAPABILITIES_CACHE_MS);
|
|
127
|
-
this.logger.debug("Fetched new server capabilities");
|
|
128
|
-
} catch (e) {
|
|
129
|
-
this.clearTimeouts();
|
|
130
|
-
const howLong = Math.floor(CAPABILITIES_RETRY_MS + Math.random() * 5000);
|
|
131
|
-
this.retryTimeout = setTimeout(this.poll, howLong);
|
|
132
|
-
this.logger.warn(`Failed to refresh capabilities: retrying in ${howLong}ms`, e);
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
private clearTimeouts(): void {
|
|
137
|
-
if (this.refreshTimeout) {
|
|
138
|
-
clearInterval(this.refreshTimeout);
|
|
139
|
-
this.refreshTimeout = undefined;
|
|
140
|
-
}
|
|
141
|
-
if (this.retryTimeout) {
|
|
142
|
-
clearTimeout(this.retryTimeout);
|
|
143
|
-
this.retryTimeout = undefined;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
87
|
}
|
package/src/store/index.ts
CHANGED
|
@@ -273,6 +273,14 @@ export interface IStore {
|
|
|
273
273
|
*/
|
|
274
274
|
getUserProfile(userId: string): Promise<SyncUserProfile | undefined>;
|
|
275
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Remove all stored events matching the given IDs from the stored accumulated
|
|
278
|
+
* sync.
|
|
279
|
+
* @param roomId The target room.
|
|
280
|
+
* @param eventIds IDs of events to remove.
|
|
281
|
+
*/
|
|
282
|
+
removeEventsFromRoom(roomId: string, eventIds: string[]): Promise<void>;
|
|
283
|
+
|
|
276
284
|
/**
|
|
277
285
|
* Stop the store and perform any appropriate cleanup
|
|
278
286
|
*/
|
|
@@ -633,6 +633,16 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
|
|
633
633
|
await txnAsPromise(txn);
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
+
public async removeEventsFromRoom(roomId: string, eventIds: string[]): Promise<void> {
|
|
637
|
+
try {
|
|
638
|
+
this.syncAccumulator.removeEventsFromRoom(roomId, eventIds);
|
|
639
|
+
const syncData = this.syncAccumulator.getJSON(true);
|
|
640
|
+
await Promise.all([this.persistSyncData(syncData.nextBatch, syncData.roomsData)]);
|
|
641
|
+
} finally {
|
|
642
|
+
this.syncToDatabasePromise = undefined;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
636
646
|
/*
|
|
637
647
|
* Close the database
|
|
638
648
|
*/
|