feat(federation): add soft-delete GC for empty group DMs with 24h grace period

Replace the hard-delete in the leave handler with a soft-delete (sets
deleted_at timestamp) when the last member leaves a group DM. A new
janitor sweep in the federation worker runs hourly and purges channels
whose grace period has expired, cascading through reactions, embeds,
attachments, messages, members, outbox/mutation-log/file-queue entries,
and finally the channel itself.

All client-facing dm_channels queries now filter on deleted_at IS NULL
to hide soft-deleted channels from the REST API and WebSocket ready
payload.
This commit is contained in:
Jannis Braun
2026-03-26 20:35:55 +01:00
parent 62a16e884a
commit 4efa35f311
4 changed files with 177 additions and 45 deletions
+3 -3
View File
@@ -2,7 +2,7 @@ import type { FastifyInstance } from 'fastify';
import type { WebSocket } from 'ws';
import { verifyJwt } from '../utils/auth.js';
import { getDb, schema } from '../db/index.js';
import { eq, and, inArray, desc, sql } from 'drizzle-orm';
import { eq, and, inArray, isNull, desc, sql } from 'drizzle-orm';
import { handleClientEvent } from './events.js';
import { computePermissions, PermissionBits, permissionsToString } from '../utils/permissions.js';
import type {
@@ -1050,10 +1050,10 @@ function buildReadyPayload(userId: string): {
const dmChannels: DmChannel[] = [];
if (dmChannelIds.length > 0) {
// Batch: all DM channels (1 query)
// Batch: all DM channels (1 query, exclude soft-deleted)
const allDmChannelRows = batchInArray(
dmChannelIds,
ids => db.select().from(schema.dmChannels).where(inArray(schema.dmChannels.id, ids)).all(),
ids => db.select().from(schema.dmChannels).where(and(inArray(schema.dmChannels.id, ids), isNull(schema.dmChannels.deletedAt))).all(),
);
const dmChannelMap = new Map(allDmChannelRows.map(c => [c.id, c]));