fix: orphaned DM cleanup runs in both modes, nuke deletes space messages

- Orphaned DM channels (zero members) are unreachable garbage — clean
  them up regardless of purgeContent mode, not just in full/nuke mode.
- Full/nuke mode now also deletes the user's space messages, their
  attachments, and embeds. This is the meaningful distinction: "Delete
  User" preserves all content as "Deleted User", "Nuke" removes it.
This commit is contained in:
Jannis Braun
2026-04-03 03:50:52 +02:00
parent c0e6c4019d
commit 32133d20cc
+53 -31
View File
@@ -3,7 +3,7 @@ import { eq, or, and, inArray } from 'drizzle-orm';
import { getDb, schema } from '../db/index.js'; import { getDb, schema } from '../db/index.js';
export interface TombstoneOptions { export interface TombstoneOptions {
/** When false, skip reaction deletion and orphaned DM purge (soft-delete mode). Default: true */ /** When false, skip space message/reaction deletion (soft-delete mode). Orphaned DM cleanup always runs. Default: true */
purgeContent?: boolean; purgeContent?: boolean;
} }
@@ -85,39 +85,61 @@ export function tombstoneUser(uid: string, options?: TombstoneOptions): string[]
} }
} }
if (purge) { // Clean up orphaned DM channels (zero members after our removal) — always runs,
// Clean up orphaned DM channels (zero members after our removal) // orphaned channels are unreachable garbage regardless of purge mode
const orphanedDmIds = tx.select({ id: schema.dmChannels.id }) const orphanedDmIds = tx.select({ id: schema.dmChannels.id })
.from(schema.dmChannels) .from(schema.dmChannels)
.all() .all()
.filter(dc => { .filter(dc => {
const memberCount = tx.select({ id: schema.dmMembers.dmChannelId }) const memberCount = tx.select({ id: schema.dmMembers.dmChannelId })
.from(schema.dmMembers) .from(schema.dmMembers)
.where(eq(schema.dmMembers.dmChannelId, dc.id)) .where(eq(schema.dmMembers.dmChannelId, dc.id))
.all()
.length;
return memberCount === 0;
})
.map(dc => dc.id);
for (const dmId of orphanedDmIds) {
const msgIds = tx.select({ id: schema.dmMessages.id })
.from(schema.dmMessages)
.where(eq(schema.dmMessages.dmChannelId, dmId))
.all() .all()
.map(m => m.id); .length;
return memberCount === 0;
})
.map(dc => dc.id);
if (msgIds.length > 0) { for (const dmId of orphanedDmIds) {
const dmAttachments = tx.select({ filename: schema.attachments.filename }) const msgIds = tx.select({ id: schema.dmMessages.id })
.from(schema.attachments) .from(schema.dmMessages)
.where(inArray(schema.attachments.dmMessageId, msgIds)) .where(eq(schema.dmMessages.dmChannelId, dmId))
.all(); .all()
for (const att of dmAttachments) filesToDelete.push(att.filename); .map(m => m.id);
tx.delete(schema.attachments).where(inArray(schema.attachments.dmMessageId, msgIds)).run(); if (msgIds.length > 0) {
tx.delete(schema.dmReactions).where(inArray(schema.dmReactions.dmMessageId, msgIds)).run(); const dmAttachments = tx.select({ filename: schema.attachments.filename })
} .from(schema.attachments)
tx.delete(schema.dmChannels).where(eq(schema.dmChannels.id, dmId)).run(); .where(inArray(schema.attachments.dmMessageId, msgIds))
.all();
for (const att of dmAttachments) filesToDelete.push(att.filename);
tx.delete(schema.attachments).where(inArray(schema.attachments.dmMessageId, msgIds)).run();
tx.delete(schema.dmReactions).where(inArray(schema.dmReactions.dmMessageId, msgIds)).run();
}
tx.delete(schema.dmChannels).where(eq(schema.dmChannels.id, dmId)).run();
}
// Purge mode: delete the user's space messages and their attachments
if (purge) {
const userMessageIds = tx.select({ id: schema.messages.id })
.from(schema.messages)
.where(eq(schema.messages.userId, uid))
.all()
.map(m => m.id);
if (userMessageIds.length > 0) {
// Collect attachment files for disk cleanup
const msgAttachments = tx.select({ filename: schema.attachments.filename })
.from(schema.attachments)
.where(inArray(schema.attachments.messageId, userMessageIds))
.all();
for (const att of msgAttachments) filesToDelete.push(att.filename);
// Delete attachments, embeds, then messages (FK order)
tx.delete(schema.attachments).where(inArray(schema.attachments.messageId, userMessageIds)).run();
tx.delete(schema.embeds).where(inArray(schema.embeds.messageId, userMessageIds)).run();
tx.delete(schema.messages).where(eq(schema.messages.userId, uid)).run();
} }
} }