feat: implement scheduled cleanup job for expired data

This commit is contained in:
syntaxbullet
2026-01-06 17:44:08 +01:00
parent 606d83a7ae
commit bc89ddf7c0
4 changed files with 262 additions and 70 deletions

View File

@@ -0,0 +1,118 @@
import { lootdrops, userTimers, userQuests } from "@/db/schema";
import { eq, and, lt, isNull, sql } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient";
import { AuroraClient } from "@/lib/BotClient";
import { env } from "@/lib/env";
import { config } from "@/lib/config";
export const cleanupService = {
/**
* Runs all cleanup tasks
*/
runAll: async () => {
console.log("🧹 Starting system cleanup...");
const stats = {
lootdrops: 0,
timers: 0,
quests: 0
};
try {
stats.lootdrops = await cleanupService.cleanupLootdrops();
stats.timers = await cleanupService.cleanupTimers();
stats.quests = await cleanupService.cleanupQuests();
console.log(`✅ Cleanup finished. Stats:
- Lootdrops: ${stats.lootdrops} removed
- Timers: ${stats.timers} removed
- Quests: ${stats.quests} cleaned`);
} catch (error) {
console.error("❌ Error during cleanup:", error);
}
},
/**
* Deletes unclaimed expired lootdrops
*/
cleanupLootdrops: async (): Promise<number> => {
const now = new Date();
const result = await DrizzleClient.delete(lootdrops)
.where(and(
lt(lootdrops.expiresAt, now),
isNull(lootdrops.claimedBy)
))
.returning({ id: lootdrops.messageId });
return result.length;
},
/**
* Cleans up expired user timers and handles side effects (like role removal)
*/
cleanupTimers: async (): Promise<number> => {
const now = new Date();
let deletedCount = 0;
// 1. Specific handling for ACCESS timers (revoking roles etc)
// This is migrated from scheduler.ts
const expiredAccess = await DrizzleClient.query.userTimers.findMany({
where: and(
eq(userTimers.type, 'ACCESS'),
lt(userTimers.expiresAt, now)
)
});
for (const timer of expiredAccess) {
const meta = timer.metadata as any;
const userIdStr = timer.userId.toString();
if (timer.key.startsWith('role_')) {
try {
const roleId = meta?.roleId || timer.key.replace('role_', '');
const guildId = env.DISCORD_GUILD_ID;
if (guildId) {
const guild = await AuroraClient.guilds.fetch(guildId);
const member = await guild.members.fetch(userIdStr);
await member.roles.remove(roleId);
console.log(`👋 Removed temporary role ${roleId} from ${member.user.tag}`);
}
} catch (err) {
console.error(`Failed to remove role for user ${userIdStr}:`, err);
}
}
// Delete specifically this one
await DrizzleClient.delete(userTimers)
.where(and(
eq(userTimers.userId, timer.userId),
eq(userTimers.type, timer.type),
eq(userTimers.key, timer.key)
));
deletedCount++;
}
// 2. Bulk delete all other expired timers (COOLDOWN, EFFECT, etc)
const result = await DrizzleClient.delete(userTimers)
.where(lt(userTimers.expiresAt, now))
.returning({ userId: userTimers.userId });
deletedCount += result.length;
return deletedCount;
},
/**
* Deletes or archives old completed quests
*/
cleanupQuests: async (): Promise<number> => {
const archiveDays = config.system.cleanup.questArchiveDays;
const threshold = new Date();
threshold.setDate(threshold.getDate() - archiveDays);
const result = await DrizzleClient.delete(userQuests)
.where(lt(userQuests.completedAt, threshold))
.returning({ userId: userQuests.userId });
return result.length;
}
};