init
This commit is contained in:
192
node_modules/matrix-js-sdk/lib/models/room-retention.js
generated
vendored
Normal file
192
node_modules/matrix-js-sdk/lib/models/room-retention.js
generated
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "../models/event.js";
|
||||
import { RoomEvent } from "./room.js";
|
||||
import { RoomStateEvent } from "./room-state.js";
|
||||
import { logger as rootLogger } from "../logger.js";
|
||||
import { EventType } from "../@types/event.js";
|
||||
import { ROOM_RETENTION_TYPE } from "../@types/retention.js";
|
||||
|
||||
/**
|
||||
* Applies https://github.com/matrix-org/matrix-spec-proposals/pull/1763 by checking the current
|
||||
* state of the room and applying the *latest* retention policy to all preceeding events.
|
||||
* Currently does not:
|
||||
* - Apply global server policies (requires Synapse work)
|
||||
* - React at all to `min_retention` configs.
|
||||
* - Loop
|
||||
*/
|
||||
export class RoomRetentionPolicy {
|
||||
constructor(room, retentionService, store) {
|
||||
// minRetention is not implemented.
|
||||
_defineProperty(this, "maxRetention", null);
|
||||
_defineProperty(this, "retentionTimeout", void 0);
|
||||
_defineProperty(this, "logger", void 0);
|
||||
_defineProperty(this, "handleRetentionUpdate", async () => {
|
||||
// First store if we currently have a retention period.
|
||||
const hadRetentionPeriod = this.maxRetention !== null;
|
||||
try {
|
||||
await this.recalculateRetention(this.room.currentState);
|
||||
} catch (err) {
|
||||
this.logger.warn("Failed to recalculate retention policy", err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bind/unbind the Timeline event handler based on whether we should be running retention.
|
||||
if (hadRetentionPeriod !== (this.maxRetention !== null)) {
|
||||
this.logger.info("retention recalculated to be", this.maxRetention);
|
||||
// We've changed
|
||||
if (this.maxRetention) {
|
||||
this.room.on(RoomEvent.Timeline, this.timelineUpdated);
|
||||
} else {
|
||||
this.room.off(RoomEvent.Timeline, this.timelineUpdated);
|
||||
}
|
||||
}
|
||||
});
|
||||
_defineProperty(this, "recalculateRetention", async roomState => {
|
||||
const unstableEvent = roomState.getStateEvents(ROOM_RETENTION_TYPE.name).find(e => e.getStateKey() === "");
|
||||
const stableEvent = roomState.getStateEvents(ROOM_RETENTION_TYPE.altName).find(e => e.getStateKey() === "");
|
||||
const serverPolicy = this.retentionService.getCached();
|
||||
const serverRoomPolicy = serverPolicy?.policies?.[this.room.roomId];
|
||||
const roomStatePolicy = unstableEvent?.getContent() ?? stableEvent?.getContent();
|
||||
const content =
|
||||
// * if the homeserver defines a specific retention policy for this room, then use this policy as the effective retention policy of the room.
|
||||
serverRoomPolicy ??
|
||||
// * otherwise, if the state of the room includes a `m.room.retention` event with an empty state key:
|
||||
roomStatePolicy ??
|
||||
// * otherwise, if the state of the room does not include a m.room.retention event with an empty state key:
|
||||
serverPolicy?.policies?.["*"];
|
||||
if (!content) {
|
||||
// * otherwise, don't apply a retention policy in this room.
|
||||
this.maxRetention = null;
|
||||
return;
|
||||
}
|
||||
const validatePolicy = !serverRoomPolicy && roomStatePolicy;
|
||||
|
||||
// Parse it
|
||||
let {
|
||||
max_lifetime: maxLifetime
|
||||
} = content;
|
||||
if (!maxLifetime && validatePolicy) {
|
||||
// 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).
|
||||
maxLifetime = serverPolicy?.limits?.max_lifetime?.min;
|
||||
if (!maxLifetime) {
|
||||
this.maxRetention = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (typeof maxLifetime !== "number") {
|
||||
throw Error(`max_lifetime must be a number, got "${maxLifetime}"`);
|
||||
}
|
||||
if (maxLifetime < 0 || !Number.isInteger(maxLifetime)) {
|
||||
throw Error(`max_lifetime must be >= 0, got ${maxLifetime}`);
|
||||
}
|
||||
this.maxRetention = maxLifetime;
|
||||
if (validatePolicy && serverPolicy?.limits?.max_lifetime) {
|
||||
// We're using room state so we need to validate this policy matches the server.
|
||||
/**
|
||||
if the value specified in the room's state complies with the limit, use this value for the effective retention policy of the room.
|
||||
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.
|
||||
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.
|
||||
*/
|
||||
this.maxRetention = Math.max(this.maxRetention, serverPolicy.limits.max_lifetime.min ?? 0);
|
||||
this.maxRetention = Math.min(this.maxRetention, serverPolicy.limits.max_lifetime.max ?? Number.MAX_SAFE_INTEGER);
|
||||
}
|
||||
this.processTimeline();
|
||||
});
|
||||
_defineProperty(this, "timelineUpdated", () => {
|
||||
this.logger.info("timelineUpdated");
|
||||
this.scheduleTimelineCheck(200);
|
||||
});
|
||||
_defineProperty(this, "scheduleTimelineCheck", nextTs => {
|
||||
if (this.retentionTimeout) {
|
||||
clearTimeout(this.retentionTimeout);
|
||||
}
|
||||
this.retentionTimeout = setTimeout(() => {
|
||||
this.processTimeline();
|
||||
this.retentionTimeout = undefined;
|
||||
}, nextTs);
|
||||
});
|
||||
this.room = room;
|
||||
this.retentionService = retentionService;
|
||||
this.store = store;
|
||||
this.logger = rootLogger.getChild(`RetentionPolicy ${room.roomId}`);
|
||||
|
||||
// Recalculate on room state update.
|
||||
room.on(RoomStateEvent.Events, event => {
|
||||
if (event.getStateKey() !== "" || event.getType() !== "org.matrix.msc1763.retention" && event.getType() !== "m.room.retention") {
|
||||
return;
|
||||
}
|
||||
void this.handleRetentionUpdate();
|
||||
});
|
||||
|
||||
// Recalculate our retention whenever the global policy changes.
|
||||
this.retentionService.on("update", () => {
|
||||
this.logger.info("Got global retention policy update!");
|
||||
void this.handleRetentionUpdate();
|
||||
});
|
||||
// Do an initial check on construction.
|
||||
void this.handleRetentionUpdate();
|
||||
|
||||
// Only bind RoomEvent.Timeline once we know we have a retention policy.
|
||||
}
|
||||
shouldEventBeRetained(ev) {
|
||||
if (!this.maxRetention) {
|
||||
return true;
|
||||
}
|
||||
const expireBefore = Date.now() - this.maxRetention;
|
||||
return ev.getTs() > expireBefore;
|
||||
}
|
||||
processTimeline() {
|
||||
if (!this.maxRetention) {
|
||||
return; // No policy, skip.
|
||||
}
|
||||
this.logger.info(`Running processTimeline`);
|
||||
const expireBefore = Date.now() - this.maxRetention;
|
||||
const events = this.room.getLiveTimeline().getEvents().filter(ev => ev.getStateKey() === undefined && !ev.isRedacted() && ev.getType() !== "m.room.redaction");
|
||||
const expiredEvents = events.filter(ev => ev.getTs() < expireBefore);
|
||||
const [earliestExpiringEvent] = events.filter(ev => ev.getTs() >= expireBefore).sort((a, b) => a.getTs() - b.getTs());
|
||||
if (earliestExpiringEvent) {
|
||||
const nextTs = earliestExpiringEvent.getTs() + this.maxRetention - Date.now();
|
||||
this.logger.info(`Next expiry scheduled for ${nextTs}`);
|
||||
this.scheduleTimelineCheck(nextTs);
|
||||
}
|
||||
if (expiredEvents.length === 0) {
|
||||
this.logger.info("Found no expired events");
|
||||
return;
|
||||
}
|
||||
this.logger.info(`Found ${expiredEvents.length} expired events`);
|
||||
for (const event of expiredEvents) {
|
||||
this.room.tryApplyRedaction(new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
sender: event.getSender(),
|
||||
event_id: "$synthetic_redaction",
|
||||
room_id: this.room.roomId,
|
||||
redacts: event.getId(),
|
||||
content: {
|
||||
reason: "Retention policy"
|
||||
},
|
||||
origin_server_ts: Date.now(),
|
||||
unsigned: {}
|
||||
}));
|
||||
}
|
||||
void this.store.removeEventsFromRoom(this.room.roomId, expiredEvents.map(e => e.getId())).catch(ex => {
|
||||
this.logger.warn(`Failed to remove events from store`, ex);
|
||||
});
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=room-retention.js.map
|
||||
Reference in New Issue
Block a user