182 lines
4.8 KiB
TypeScript
182 lines
4.8 KiB
TypeScript
import { and, eq, isNull, lt } from "drizzle-orm";
|
|
import { getDatabase } from "../database/drizzle.js";
|
|
import {
|
|
attachmentsTable,
|
|
messagesTable,
|
|
retentionPoliciesTable,
|
|
voiceRecordingsTable,
|
|
} from "../database/schema.js";
|
|
import { createChildLogger } from "../logger.js";
|
|
import { getRetentionPolicy } from "./messageStore.js";
|
|
import type { RetentionPolicy } from "./types.js";
|
|
|
|
const logger = createChildLogger("retention-manager");
|
|
|
|
interface RetentionResult {
|
|
messagesDeleted: number;
|
|
attachmentsDeleted: number;
|
|
voiceRecordingsDeleted: number;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* Executes retention policy for a guild
|
|
* Deletes messages, attachments, and voice recordings older than retention_days
|
|
*/
|
|
export async function executeRetentionPolicy(
|
|
guildId: string,
|
|
): Promise<RetentionResult> {
|
|
const result: RetentionResult = {
|
|
messagesDeleted: 0,
|
|
attachmentsDeleted: 0,
|
|
voiceRecordingsDeleted: 0,
|
|
};
|
|
|
|
try {
|
|
const policy = await getRetentionPolicy(guildId);
|
|
if (!policy || !policy.enabled) {
|
|
logger.debug({ guildId }, "Retention policy not enabled");
|
|
return result;
|
|
}
|
|
|
|
const db = getDatabase() as any;
|
|
const cutoffTime = Date.now() - policy.retention_days * 24 * 60 * 60 * 1000;
|
|
|
|
// Delete old messages
|
|
const deletedMessages = await db
|
|
.delete(messagesTable)
|
|
.where(
|
|
and(
|
|
eq(messagesTable.guild_id, guildId),
|
|
lt(messagesTable.created_at, cutoffTime),
|
|
isNull(messagesTable.deleted_at),
|
|
),
|
|
);
|
|
|
|
result.messagesDeleted = deletedMessages.rowsAffected || 0;
|
|
|
|
// Delete old attachments if policy applies
|
|
if (policy.apply_to_media) {
|
|
const deletedAttachments = await db
|
|
.delete(attachmentsTable)
|
|
.where(
|
|
and(
|
|
eq(attachmentsTable.guild_id, guildId),
|
|
lt(attachmentsTable.created_at, cutoffTime),
|
|
),
|
|
);
|
|
|
|
result.attachmentsDeleted = deletedAttachments.rowsAffected || 0;
|
|
}
|
|
|
|
// Delete old voice recordings if policy applies
|
|
if (policy.apply_to_voice) {
|
|
const deletedVoice = await db
|
|
.delete(voiceRecordingsTable)
|
|
.where(
|
|
and(
|
|
eq(voiceRecordingsTable.guild_id, guildId),
|
|
lt(voiceRecordingsTable.created_at, cutoffTime),
|
|
),
|
|
);
|
|
|
|
result.voiceRecordingsDeleted = deletedVoice.rowsAffected || 0;
|
|
}
|
|
|
|
logger.info(
|
|
{
|
|
guildId,
|
|
retentionDays: policy.retention_days,
|
|
...result,
|
|
},
|
|
"Retention policy executed",
|
|
);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
logger.error(
|
|
{ guildId, error: message },
|
|
"Failed to execute retention policy",
|
|
);
|
|
result.error = message;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes retention policies for all enabled guilds
|
|
* Returns summary of deletions
|
|
*/
|
|
export async function executeAllRetentionPolicies(): Promise<{
|
|
policiesExecuted: number;
|
|
totalMessagesDeleted: number;
|
|
totalAttachmentsDeleted: number;
|
|
totalVoiceDeleted: number;
|
|
errors: Array<{ guildId: string; error: string }>;
|
|
}> {
|
|
const summary = {
|
|
policiesExecuted: 0,
|
|
totalMessagesDeleted: 0,
|
|
totalAttachmentsDeleted: 0,
|
|
totalVoiceDeleted: 0,
|
|
errors: [] as Array<{ guildId: string; error: string }>,
|
|
};
|
|
|
|
try {
|
|
const db = getDatabase() as any;
|
|
const policies = await db
|
|
.select()
|
|
.from(retentionPoliciesTable)
|
|
.where(eq(retentionPoliciesTable.enabled, true));
|
|
|
|
for (const policy of policies as RetentionPolicy[]) {
|
|
const result = await executeRetentionPolicy(policy.guild_id);
|
|
summary.policiesExecuted++;
|
|
summary.totalMessagesDeleted += result.messagesDeleted;
|
|
summary.totalAttachmentsDeleted += result.attachmentsDeleted;
|
|
summary.totalVoiceDeleted += result.voiceRecordingsDeleted;
|
|
|
|
if (result.error) {
|
|
summary.errors.push({
|
|
guildId: policy.guild_id,
|
|
error: result.error,
|
|
});
|
|
}
|
|
}
|
|
|
|
logger.info(summary, "All retention policies executed");
|
|
return summary;
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
logger.error(
|
|
{ error: message },
|
|
"Failed to execute all retention policies",
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts a periodic retention policy executor
|
|
* Runs every 24 hours by default
|
|
*/
|
|
export function startRetentionPolicyWorker(
|
|
intervalMs: number = 24 * 60 * 60 * 1000,
|
|
): NodeJS.Timeout {
|
|
logger.info({ intervalMs }, "Starting retention policy worker");
|
|
|
|
const interval = setInterval(async () => {
|
|
try {
|
|
await executeAllRetentionPolicies();
|
|
} catch (error) {
|
|
logger.error(
|
|
{ error: error instanceof Error ? error.message : String(error) },
|
|
"Retention policy worker failed",
|
|
);
|
|
}
|
|
}, intervalMs);
|
|
|
|
return interval;
|
|
}
|