From 7a726c335dbb46ed687402536c7b4fcd1a741527 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Sat, 14 Mar 2026 00:52:41 +0900 Subject: [PATCH] feat: purge status channel on startup for clean dashboard - Add purgeChannel() to Channel interface and DiscordChannel - Uses bulkDelete for recent messages, individual delete for old ones - Called once at startup before creating fresh dashboard messages --- src/channels/discord.ts | 47 +++++++++++++++++++++++++++++++++++++++++ src/index.ts | 15 ++++++++++++- src/types.ts | 2 ++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/channels/discord.ts b/src/channels/discord.ts index c6980fe..56bb619 100644 --- a/src/channels/discord.ts +++ b/src/channels/discord.ts @@ -494,6 +494,53 @@ export class DiscordChannel implements Channel { return result; } + async purgeChannel(jid: string): Promise { + if (!this.client) return 0; + let deleted = 0; + try { + const channelId = jid.replace(/^dc:/, ''); + const channel = await this.client.channels.fetch(channelId); + if (!channel || !('bulkDelete' in channel)) return 0; + const tc = channel as TextChannel; + + // Fetch and delete in batches (bulkDelete handles up to 100, only < 14 days old) + let hasMore = true; + while (hasMore) { + const messages = await tc.messages.fetch({ limit: 100 }); + if (messages.size === 0) break; + + // Separate into bulk-deletable (< 14 days) and old messages + const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000; + const recent = messages.filter( + (m) => m.createdTimestamp > twoWeeksAgo, + ); + const old = messages.filter( + (m) => m.createdTimestamp <= twoWeeksAgo, + ); + + if (recent.size >= 2) { + await tc.bulkDelete(recent); + deleted += recent.size; + } else if (recent.size === 1) { + await recent.first()!.delete(); + deleted += 1; + } + + for (const [, msg] of old) { + await msg.delete(); + deleted += 1; + } + + hasMore = messages.size === 100; + } + + logger.info({ jid, deleted }, 'Purged channel messages'); + } catch (err) { + logger.error({ jid, err, deleted }, 'Failed to purge channel messages'); + } + return deleted; + } + async editMessage( jid: string, messageId: string, diff --git a/src/index.ts b/src/index.ts index f698543..de62e13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -756,7 +756,10 @@ async function buildUsageContent(): Promise { lines.push(`• 업타임: ${formatElapsed(os.uptime() * 1000)}`); - return lines.join('\n') + `\n\n_${new Date().toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })}_`; + return ( + lines.join('\n') + + `\n\n_${new Date().toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })}_` + ); } // ── Dashboard Lifecycle ───────────────────────────────────────── @@ -1115,6 +1118,16 @@ async function main(): Promise { }); queue.setProcessMessagesFn(processGroupMessages); recoverPendingMessages(); + // Purge old messages in status channel before creating fresh dashboards + if (STATUS_CHANNEL_ID) { + const statusJid = `dc:${STATUS_CHANNEL_ID}`; + const ch = channels.find( + (c) => c.name.startsWith('discord') && c.isConnected() && c.purgeChannel, + ); + if (ch?.purgeChannel) { + await ch.purgeChannel(statusJid); + } + } await startStatusDashboard(); await startUsageDashboard(); startMessageLoop().catch((err) => { diff --git a/src/types.ts b/src/types.ts index 03472c9..316b284 100644 --- a/src/types.ts +++ b/src/types.ts @@ -88,6 +88,8 @@ export interface Channel { syncGroups?(force: boolean): Promise; // Optional: get channel metadata (position, category) for ordering. getChannelMeta?(jids: string[]): Promise>; + // Optional: delete all messages in a channel (used for dashboard cleanup). + purgeChannel?(jid: string): Promise; } // Callback type that channels use to deliver inbound messages