2026-07-03 11:23:14 -04:00
|
|
|
import { Room } from 'matrix-js-sdk';
|
|
|
|
|
import { StateEvent } from '../../types/matrix/room';
|
|
|
|
|
|
|
|
|
|
// MSC1763 — per-room message retention (`m.room.retention`). `max_lifetime` is a
|
|
|
|
|
// duration in milliseconds after which a message is considered expired.
|
|
|
|
|
export type RetentionContent = {
|
|
|
|
|
max_lifetime?: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
// Floor to avoid foot-guns (an admin fat-fingering a tiny value nuking a room).
|
|
|
|
|
export const RETENTION_MIN_MS = 10 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
export type RetentionPreset = { label: string; ms: number };
|
|
|
|
|
export const RETENTION_PRESETS: RetentionPreset[] = [
|
|
|
|
|
{ label: 'Off', ms: 0 },
|
|
|
|
|
{ label: '1 Day', ms: DAY_MS },
|
|
|
|
|
{ label: '1 Week', ms: 7 * DAY_MS },
|
|
|
|
|
{ label: '1 Month', ms: 30 * DAY_MS },
|
|
|
|
|
];
|
|
|
|
|
|
2026-10-16 21:37:16 -04:00
|
|
|
/**
|
|
|
|
|
* [Gitea #298] A usable retention window, or `undefined`. Anything under
|
|
|
|
|
* RETENTION_MIN_MS is treated as off: a fat-fingered `max_lifetime: 1000` (set
|
|
|
|
|
* from any client by anyone with state power) used to blank every timeline and
|
|
|
|
|
* get opted-in users' messages redacted within a minute.
|
|
|
|
|
*/
|
|
|
|
|
export const effectiveRetentionMs = (maxLifetime: unknown): number | undefined =>
|
|
|
|
|
typeof maxLifetime === 'number' && Number.isFinite(maxLifetime) && maxLifetime >= RETENTION_MIN_MS
|
|
|
|
|
? maxLifetime
|
|
|
|
|
: undefined;
|
|
|
|
|
|
2026-07-03 11:23:14 -04:00
|
|
|
/** The room's active retention window in ms, or `undefined` when unset/disabled. */
|
|
|
|
|
export const getRoomRetentionMs = (room: Room): number | undefined => {
|
|
|
|
|
const event = room.currentState.getStateEvents(StateEvent.RoomRetention, '');
|
2026-10-16 21:37:16 -04:00
|
|
|
return effectiveRetentionMs(event?.getContent<RetentionContent>()?.max_lifetime);
|
2026-07-03 11:23:14 -04:00
|
|
|
};
|
|
|
|
|
|
2026-10-16 21:37:16 -04:00
|
|
|
/**
|
|
|
|
|
* [Gitea #298] Deletion grace: the auto-delete sweeper ignores a retention
|
|
|
|
|
* policy for this long after it was set, so a mistaken or malicious change can
|
|
|
|
|
* be reverted before anything is permanently redacted. (Hiding expired
|
|
|
|
|
* messages locally is not destructive and applies at once.)
|
|
|
|
|
*/
|
|
|
|
|
export const RETENTION_DELETE_GRACE_MS = DAY_MS;
|
|
|
|
|
|
|
|
|
|
export const retentionDeletesAllowed = (policySetAtMs: number, nowMs: number): boolean =>
|
|
|
|
|
nowMs - policySetAtMs >= RETENTION_DELETE_GRACE_MS;
|
|
|
|
|
|
|
|
|
|
/** When the room's current retention policy was set (its state event's ts). */
|
|
|
|
|
export const getRoomRetentionSetAt = (room: Room): number | undefined =>
|
|
|
|
|
room.currentState.getStateEvents(StateEvent.RoomRetention, '')?.getTs();
|
|
|
|
|
|
2026-07-03 11:23:14 -04:00
|
|
|
/** True when an event at `tsMs` has passed the `maxLifetimeMs` retention window. */
|
|
|
|
|
export const isExpired = (tsMs: number, maxLifetimeMs: number, nowMs: number): boolean =>
|
|
|
|
|
maxLifetimeMs > 0 && nowMs - tsMs > maxLifetimeMs;
|