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.
Files changed (95) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/@types/event.d.ts +6 -1
  3. package/lib/@types/event.d.ts.map +1 -1
  4. package/lib/@types/event.js +3 -0
  5. package/lib/@types/event.js.map +1 -1
  6. package/lib/@types/retention.d.ts +15 -0
  7. package/lib/@types/retention.d.ts.map +1 -0
  8. package/lib/@types/retention.js +13 -0
  9. package/lib/@types/retention.js.map +1 -0
  10. package/lib/capabilityPoller.d.ts +38 -0
  11. package/lib/capabilityPoller.d.ts.map +1 -0
  12. package/lib/capabilityPoller.js +96 -0
  13. package/lib/capabilityPoller.js.map +1 -0
  14. package/lib/client.d.ts +7 -0
  15. package/lib/client.d.ts.map +1 -1
  16. package/lib/client.js +15 -5
  17. package/lib/client.js.map +1 -1
  18. package/lib/matrix.d.ts +1 -0
  19. package/lib/matrix.d.ts.map +1 -1
  20. package/lib/matrix.js +1 -0
  21. package/lib/matrix.js.map +1 -1
  22. package/lib/matrixrtc/MembershipManager.d.ts +2 -0
  23. package/lib/matrixrtc/MembershipManager.d.ts.map +1 -1
  24. package/lib/matrixrtc/MembershipManager.js +2 -0
  25. package/lib/matrixrtc/MembershipManager.js.map +1 -1
  26. package/lib/matrixrtc/MembershipManagerActionScheduler.d.ts.map +1 -1
  27. package/lib/matrixrtc/MembershipManagerActionScheduler.js +4 -1
  28. package/lib/matrixrtc/MembershipManagerActionScheduler.js.map +1 -1
  29. package/lib/models/room-retention.d.ts +28 -0
  30. package/lib/models/room-retention.d.ts.map +1 -0
  31. package/lib/models/room-retention.js +201 -0
  32. package/lib/models/room-retention.js.map +1 -0
  33. package/lib/models/room.d.ts +2 -1
  34. package/lib/models/room.d.ts.map +1 -1
  35. package/lib/models/room.js +29 -3
  36. package/lib/models/room.js.map +1 -1
  37. package/lib/retentionPolicy.d.ts +33 -0
  38. package/lib/retentionPolicy.d.ts.map +1 -0
  39. package/lib/retentionPolicy.js +42 -0
  40. package/lib/retentionPolicy.js.map +1 -0
  41. package/lib/rust-crypto/backup.d.ts +7 -2
  42. package/lib/rust-crypto/backup.d.ts.map +1 -1
  43. package/lib/rust-crypto/backup.js +13 -4
  44. package/lib/rust-crypto/backup.js.map +1 -1
  45. package/lib/serverCapabilities.d.ts +4 -23
  46. package/lib/serverCapabilities.d.ts.map +1 -1
  47. package/lib/serverCapabilities.js +8 -61
  48. package/lib/serverCapabilities.js.map +1 -1
  49. package/lib/store/index.d.ts +7 -0
  50. package/lib/store/index.d.ts.map +1 -1
  51. package/lib/store/index.js.map +1 -1
  52. package/lib/store/indexeddb-local-backend.d.ts +1 -0
  53. package/lib/store/indexeddb-local-backend.d.ts.map +1 -1
  54. package/lib/store/indexeddb-local-backend.js +15 -3
  55. package/lib/store/indexeddb-local-backend.js.map +1 -1
  56. package/lib/store/indexeddb-remote-backend.d.ts +3 -1
  57. package/lib/store/indexeddb-remote-backend.d.ts.map +1 -1
  58. package/lib/store/indexeddb-remote-backend.js +29 -8
  59. package/lib/store/indexeddb-remote-backend.js.map +1 -1
  60. package/lib/store/indexeddb.d.ts +7 -5
  61. package/lib/store/indexeddb.d.ts.map +1 -1
  62. package/lib/store/indexeddb.js +20 -10
  63. package/lib/store/indexeddb.js.map +1 -1
  64. package/lib/store/memory.d.ts +1 -0
  65. package/lib/store/memory.d.ts.map +1 -1
  66. package/lib/store/memory.js +3 -0
  67. package/lib/store/memory.js.map +1 -1
  68. package/lib/store/stub.d.ts +1 -0
  69. package/lib/store/stub.d.ts.map +1 -1
  70. package/lib/store/stub.js +3 -0
  71. package/lib/store/stub.js.map +1 -1
  72. package/lib/sync-accumulator.d.ts +1 -0
  73. package/lib/sync-accumulator.d.ts.map +1 -1
  74. package/lib/sync-accumulator.js +4 -0
  75. package/lib/sync-accumulator.js.map +1 -1
  76. package/package.json +3 -3
  77. package/src/@types/event.ts +9 -0
  78. package/src/@types/retention.ts +16 -0
  79. package/src/capabilityPoller.ts +102 -0
  80. package/src/client.ts +18 -4
  81. package/src/matrix.ts +1 -0
  82. package/src/matrixrtc/MembershipManager.ts +2 -0
  83. package/src/matrixrtc/MembershipManagerActionScheduler.ts +4 -1
  84. package/src/models/room-retention.ts +230 -0
  85. package/src/models/room.ts +17 -1
  86. package/src/retentionPolicy.ts +65 -0
  87. package/src/rust-crypto/backup.ts +15 -4
  88. package/src/serverCapabilities.ts +8 -67
  89. package/src/store/index.ts +8 -0
  90. package/src/store/indexeddb-local-backend.ts +10 -0
  91. package/src/store/indexeddb-remote-backend.ts +24 -5
  92. package/src/store/indexeddb.ts +20 -8
  93. package/src/store/memory.ts +4 -0
  94. package/src/store/stub.ts +3 -0
  95. 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.getCachedCapabilities();
1953
+ const caps = this.serverCapabilitiesService.getCached();
1940
1954
  if (caps) return caps;
1941
- return this.serverCapabilitiesService.fetchCapabilities();
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.getCachedCapabilities();
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.fetchCapabilities();
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
- throw Error(`The MembershipManager shut down because of the end condition: ${e}`);
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
+ }
@@ -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
- private tryApplyRedaction = (event: MatrixEvent): void => {
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
- private readonly logger: Logger,
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
- // Check if the backup should be enabled (e.g. if it's properly
210
- // signed), and enable it if it should
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 { type Logger } from "./logger.ts";
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
- private capabilities?: Capabilities;
82
- private retryTimeout?: ReturnType<typeof setTimeout>;
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 fetchCapabilities = async (): Promise<Capabilities> => {
82
+ public fetch = async (): Promise<Capabilities> => {
117
83
  const resp = await this.http.authedRequest<CapabilitiesResponse>(Method.Get, "/capabilities");
118
- this.capabilities = resp["capabilities"];
119
- return this.capabilities;
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
  }
@@ -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
  */