From 76081e18e203b9f9a5e8cbd5a973af91df0bb444 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 15 Aug 2026 04:01:07 +0300 Subject: [PATCH 1/8] feat: add ARCHIVED_CATEGORY_ID env variable --- src/env.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/env.ts b/src/env.ts index c35b3b6..28da03e 100644 --- a/src/env.ts +++ b/src/env.ts @@ -41,6 +41,7 @@ export const config = { showcase: requireEnv('SHOWCASE_CHANNEL_ID'), showcaseLogs: requireEnv('SHOWCASE_LOG_CHANNEL_ID'), showcaseRules: requireEnv('SHOWCASE_RULES_CHANNEL_ID'), + archivedCategory: requireEnv('ARCHIVED_CATEGORY_ID'), }, onboarding: { channelId: optionalEnv('ONBOARDING_CHANNEL_ID'), From 7a32222dc7d391eed62276a68eca8cfa4a73f02d Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 16 Aug 2026 01:38:10 +0300 Subject: [PATCH 2/8] feat: add channel archiving --- src/common/events/index.ts | 2 + src/features/archive-channels/index.ts | 124 +++++++++++++++++++++++++ src/features/ready/index.ts | 35 ++++++- 3 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 src/features/archive-channels/index.ts diff --git a/src/common/events/index.ts b/src/common/events/index.ts index 05330c9..2b8c335 100644 --- a/src/common/events/index.ts +++ b/src/common/events/index.ts @@ -3,10 +3,12 @@ import { hasVarEvent } from '@/features/has-var/index.js'; import { interactionCreateEvent } from '@/features/interaction-create/index.js'; import { readyEvent } from '@/features/ready/index.js'; import type { DiscordEvent } from './types.js'; +import archiveChannels from '@/features/archive-channels/index.js'; export const events: DiscordEvent[] = [ readyEvent, guildCreateEvent, hasVarEvent, interactionCreateEvent, + archiveChannels, ].flat(); diff --git a/src/features/archive-channels/index.ts b/src/features/archive-channels/index.ts new file mode 100644 index 0000000..1b611b8 --- /dev/null +++ b/src/features/archive-channels/index.ts @@ -0,0 +1,124 @@ +import { createEvent } from '@/common/events/create-event.js'; +import { config } from '@/env.js'; +import { Events, PermissionFlagsBits, type GuildChannel } from 'discord.js'; + +const PUBLIC_PERMISSIONS = [ + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.CreatePublicThreads, + PermissionFlagsBits.CreatePrivateThreads, + PermissionFlagsBits.SendMessagesInThreads, + PermissionFlagsBits.Connect, +]; + +const processingChannels = new Set(); + +export default createEvent( + { + name: Events.ChannelUpdate, + }, + async (oldChannel, newChannel) => { + if (newChannel.isDMBased() || oldChannel.isDMBased()) { + return; + } + + // We only care about channels that are being moved to or out of the archived category + if ( + newChannel.parentId !== config.channelIds.archivedCategory && + oldChannel.parentId !== config.channelIds.archivedCategory + ) { + return; + } + + // Prevent deadlocks from nested ChannelUpdate events caused by our own edits + if (processingChannels.has(newChannel.id)) { + return; + } + + if (newChannel.parentId === config.channelIds.archivedCategory) { + await archiveChannel(newChannel); + } else { + await unarchiveChannel(newChannel); + } + } +); + +export async function archiveChannel(channel: GuildChannel) { + const channelName = channel.name; + + const archivedChannelName = channelName.match(/^archived-/) + ? channelName + : `archived-${channelName}`; + + if (archivedChannelName === channelName && hasArchivedPermissions(channel)) { + return; + } + + processingChannels.add(channel.id); + + try { + const renamedChannel = await channel.setName(archivedChannelName); + await setArchivedPermissions(renamedChannel, true); + } catch (error) { + console.error(`Error archiving channel ${channelName}:`, error); + } finally { + processingChannels.delete(channel.id); + } +} + +async function unarchiveChannel(channel: GuildChannel) { + const channelName = channel.name; + + const regex = /^archived-/; + + if (!channelName.match(regex)) { + return; + } + + const newChannelName = channelName.replace(regex, ''); + + processingChannels.add(channel.id); + + try { + await setArchivedPermissions(channel, false); + await channel.setName(newChannelName); + } catch (error) { + console.error( + `Error unarchiving channel ${channelName}:`, + (error as Error).message + ); + } finally { + processingChannels.delete(channel.id); + } +} + +function hasArchivedPermissions(channel: GuildChannel) { + const everyoneRole = channel.guild.roles.everyone; + const overwrite = channel.permissionOverwrites.cache.get(everyoneRole.id); + + if (!overwrite) { + return false; + } + + return PUBLIC_PERMISSIONS.every((permission) => + overwrite.deny.has(permission) + ); +} + +async function setArchivedPermissions( + channel: GuildChannel, + archived: boolean +) { + await channel.permissionOverwrites.edit( + channel.guild.roles.everyone.id, + { + SendMessages: archived ? false : null, + CreatePublicThreads: archived ? false : null, + CreatePrivateThreads: archived ? false : null, + SendMessagesInThreads: archived ? false : null, + Connect: archived ? false : null, + }, + { + reason: `${archived ? 'Archiving' : 'Unarchiving'} channel ${channel.name}`, + } + ); +} diff --git a/src/features/ready/index.ts b/src/features/ready/index.ts index a95c651..58f9a48 100644 --- a/src/features/ready/index.ts +++ b/src/features/ready/index.ts @@ -1,10 +1,11 @@ -import { Events } from 'discord.js'; +import { ChannelType, Events } from 'discord.js'; import { createEvent } from '@/common/events/create-event.js'; import { config } from '@/env.js'; import { initializeAdventScheduler } from '@/util/advent-scheduler.js'; import { fetchAndCachePublicChannelsMessages } from '@/util/cache.js'; import { syncGuidesToChannel } from '@/util/post-guides.js'; import { leaveIfNotAllowedServer } from '@/util/server-guard.js'; +import { archiveChannel } from '../archive-channels/index.js'; export const readyEvent = createEvent( { @@ -20,11 +21,17 @@ export const readyEvent = createEvent( await leaveIfNotAllowedServer(guild); } + const guild = client.guilds.cache.get(config.discord.serverId); + if (!guild) { + console.error( + `❌ Bot is not in the configured server with ID ${config.discord.serverId}` + ); + console.error('Please check your .env file or CI/CD configuration'); + process.exit(1); + } + if (config.fetchAndSyncMessages) { - const guild = client.guilds.cache.get(config.discord.serverId); - if (guild) { - await fetchAndCachePublicChannelsMessages(guild, true); - } + await fetchAndCachePublicChannelsMessages(guild, true); // Sync guides to channel try { @@ -54,5 +61,23 @@ export const readyEvent = createEvent( } catch (error) { console.error('❌ Failed to initialize Advent of Code scheduler:', error); } + + // Make sure all channels in the archived category are properly archived on startup + try { + const archivedCategory = guild.channels.cache.get( + config.channelIds.archivedCategory + ); + if (archivedCategory?.type !== ChannelType.GuildCategory) { + console.error( + `❌ Archived category with ID ${config.channelIds.archivedCategory} not found in the guild.` + ); + return; + } + + const archivedChannels = archivedCategory.children.cache; + await Promise.all(archivedChannels.map(archiveChannel)); + } catch (error) { + console.error('❌ Failed to archive channels on startup:', error); + } } ); From c3ccc9d22f18fdd698ddf01ee93ce6f631f27ea5 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 16 Aug 2026 01:39:57 +0300 Subject: [PATCH 3/8] fix: add missing test env --- .env.example | 1 + .env.production | 1 + .env.test | 1 + 3 files changed, 3 insertions(+) diff --git a/.env.example b/.env.example index bae1c1c..fb3b9be 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ GUIDES_CHANNEL_ID=your_guides_channel_id_here ADVENT_OF_CODE_CHANNEL_ID=your_advent_of_code_forum_channel_id_here REPEL_LOG_CHANNEL_ID=your_repel_log_channel_id_here ONBOARDING_CHANNEL_ID=onboarding_channel_id_here +ARCHIVED_CATEGORY_ID=archived_category_id_here # Role IDs (REQUIRED) MODERATORS_ROLE_IDS=role_id_1,role_id_2,role_id_3 diff --git a/.env.production b/.env.production index 1b04eb7..b8643ed 100644 --- a/.env.production +++ b/.env.production @@ -12,6 +12,7 @@ ADVENT_OF_CODE_CHANNEL_ID=1047623689488830495 SHOWCASE_CHANNEL_ID=1517161718818541658 SHOWCASE_LOG_CHANNEL_ID=1517565847982444634 SHOWCASE_RULES_CHANNEL_ID=1517948527098073158 +ARCHIVED_CATEGORY_ID=837507969859977258 # Role IDs (from your dev server) diff --git a/.env.test b/.env.test index c48bb66..d0da5cc 100644 --- a/.env.test +++ b/.env.test @@ -19,6 +19,7 @@ REPEL_LOG_CHANNEL_ID=your-repel-log-channel-id SHOWCASE_CHANNEL_ID=your-showcase-forum-channel-id SHOWCASE_LOG_CHANNEL_ID=your-showcase-log-channel-id SHOWCASE_RULES_CHANNEL_ID=your-showcase-rules-channel-id +ARCHIVED_CATEGORY_ID=your-archived-category-id # Role IDs (from your dev server) REPEL_ROLE_ID=your-repel-role-id From c279b635920dc8adb54fe6e2be34cbd635cecb1f Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 16 Aug 2026 17:51:16 +0300 Subject: [PATCH 4/8] refactor: extract archive regex to a const and rename archive category variable --- src/env.ts | 2 +- src/features/archive-channels/index.ts | 16 ++++++++-------- src/features/ready/index.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/env.ts b/src/env.ts index 28da03e..b42938f 100644 --- a/src/env.ts +++ b/src/env.ts @@ -41,7 +41,7 @@ export const config = { showcase: requireEnv('SHOWCASE_CHANNEL_ID'), showcaseLogs: requireEnv('SHOWCASE_LOG_CHANNEL_ID'), showcaseRules: requireEnv('SHOWCASE_RULES_CHANNEL_ID'), - archivedCategory: requireEnv('ARCHIVED_CATEGORY_ID'), + archiveCategory: requireEnv('ARCHIVED_CATEGORY_ID'), }, onboarding: { channelId: optionalEnv('ONBOARDING_CHANNEL_ID'), diff --git a/src/features/archive-channels/index.ts b/src/features/archive-channels/index.ts index 1b611b8..0611caa 100644 --- a/src/features/archive-channels/index.ts +++ b/src/features/archive-channels/index.ts @@ -10,6 +10,8 @@ const PUBLIC_PERMISSIONS = [ PermissionFlagsBits.Connect, ]; +const ARCHIVED_REGEX = /^archived-/; + const processingChannels = new Set(); export default createEvent( @@ -23,8 +25,8 @@ export default createEvent( // We only care about channels that are being moved to or out of the archived category if ( - newChannel.parentId !== config.channelIds.archivedCategory && - oldChannel.parentId !== config.channelIds.archivedCategory + newChannel.parentId !== config.channelIds.archiveCategory && + oldChannel.parentId !== config.channelIds.archiveCategory ) { return; } @@ -34,7 +36,7 @@ export default createEvent( return; } - if (newChannel.parentId === config.channelIds.archivedCategory) { + if (newChannel.parentId === config.channelIds.archiveCategory) { await archiveChannel(newChannel); } else { await unarchiveChannel(newChannel); @@ -45,7 +47,7 @@ export default createEvent( export async function archiveChannel(channel: GuildChannel) { const channelName = channel.name; - const archivedChannelName = channelName.match(/^archived-/) + const archivedChannelName = channelName.match(ARCHIVED_REGEX) ? channelName : `archived-${channelName}`; @@ -68,13 +70,11 @@ export async function archiveChannel(channel: GuildChannel) { async function unarchiveChannel(channel: GuildChannel) { const channelName = channel.name; - const regex = /^archived-/; - - if (!channelName.match(regex)) { + if (!channelName.match(ARCHIVED_REGEX)) { return; } - const newChannelName = channelName.replace(regex, ''); + const newChannelName = channelName.replace(ARCHIVED_REGEX, ''); processingChannels.add(channel.id); diff --git a/src/features/ready/index.ts b/src/features/ready/index.ts index 58f9a48..b9b8df8 100644 --- a/src/features/ready/index.ts +++ b/src/features/ready/index.ts @@ -65,11 +65,11 @@ export const readyEvent = createEvent( // Make sure all channels in the archived category are properly archived on startup try { const archivedCategory = guild.channels.cache.get( - config.channelIds.archivedCategory + config.channelIds.archiveCategory ); if (archivedCategory?.type !== ChannelType.GuildCategory) { console.error( - `❌ Archived category with ID ${config.channelIds.archivedCategory} not found in the guild.` + `❌ Archived category with ID ${config.channelIds.archiveCategory} not found in the guild.` ); return; } From ed6f8623283598a2970aa03c82c6b3a0fe4ffb3b Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 16 Aug 2026 18:20:33 +0300 Subject: [PATCH 5/8] refactor: move archived channel startup check into archive feature --- src/features/archive-channels/index.ts | 32 +++++++++++++++++++++++++- src/features/ready/index.ts | 22 ++++++------------ 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/features/archive-channels/index.ts b/src/features/archive-channels/index.ts index 0611caa..ad67de1 100644 --- a/src/features/archive-channels/index.ts +++ b/src/features/archive-channels/index.ts @@ -1,6 +1,12 @@ import { createEvent } from '@/common/events/create-event.js'; import { config } from '@/env.js'; -import { Events, PermissionFlagsBits, type GuildChannel } from 'discord.js'; +import { + ChannelType, + Events, + Guild, + PermissionFlagsBits, + type GuildChannel, +} from 'discord.js'; const PUBLIC_PERMISSIONS = [ PermissionFlagsBits.SendMessages, @@ -122,3 +128,27 @@ async function setArchivedPermissions( } ); } + +export async function ensureArchivedChannelsAreProperlyArchived(guild: Guild) { + const archiveCategory = guild.channels.cache.get( + config.channelIds.archiveCategory + ); + + if (archiveCategory?.type !== ChannelType.GuildCategory) { + console.error( + `❌ Archive category with ID ${config.channelIds.archiveCategory} not found in the guild.` + ); + return; + } + + const archivedChannels = archiveCategory.children.cache; + const results = await Promise.allSettled( + archivedChannels.map(archiveChannel) + ); + + for (const result of results) { + if (result.status === 'rejected') { + console.error('Error archiving channel:', result.reason); + } + } +} diff --git a/src/features/ready/index.ts b/src/features/ready/index.ts index b9b8df8..fa97912 100644 --- a/src/features/ready/index.ts +++ b/src/features/ready/index.ts @@ -1,11 +1,11 @@ -import { ChannelType, Events } from 'discord.js'; +import { Events } from 'discord.js'; import { createEvent } from '@/common/events/create-event.js'; import { config } from '@/env.js'; import { initializeAdventScheduler } from '@/util/advent-scheduler.js'; import { fetchAndCachePublicChannelsMessages } from '@/util/cache.js'; import { syncGuidesToChannel } from '@/util/post-guides.js'; import { leaveIfNotAllowedServer } from '@/util/server-guard.js'; -import { archiveChannel } from '../archive-channels/index.js'; +import { ensureArchivedChannelsAreProperlyArchived } from '../archive-channels/index.js'; export const readyEvent = createEvent( { @@ -64,20 +64,12 @@ export const readyEvent = createEvent( // Make sure all channels in the archived category are properly archived on startup try { - const archivedCategory = guild.channels.cache.get( - config.channelIds.archiveCategory - ); - if (archivedCategory?.type !== ChannelType.GuildCategory) { - console.error( - `❌ Archived category with ID ${config.channelIds.archiveCategory} not found in the guild.` - ); - return; - } - - const archivedChannels = archivedCategory.children.cache; - await Promise.all(archivedChannels.map(archiveChannel)); + await ensureArchivedChannelsAreProperlyArchived(guild); } catch (error) { - console.error('❌ Failed to archive channels on startup:', error); + console.error( + '❌ Failed to ensure archived channels are properly archived:', + error + ); } } ); From df965921f01e7b8622f7d88f81b5eb85506ca7ff Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 16 Aug 2026 18:22:43 +0300 Subject: [PATCH 6/8] refactor: rename archive env variable --- .env.example | 2 +- .env.production | 2 +- .env.test | 2 +- src/env.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index fb3b9be..9713e5e 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ GUIDES_CHANNEL_ID=your_guides_channel_id_here ADVENT_OF_CODE_CHANNEL_ID=your_advent_of_code_forum_channel_id_here REPEL_LOG_CHANNEL_ID=your_repel_log_channel_id_here ONBOARDING_CHANNEL_ID=onboarding_channel_id_here -ARCHIVED_CATEGORY_ID=archived_category_id_here +ARCHIVE_CATEGORY_ID=archive_category_id_here # Role IDs (REQUIRED) MODERATORS_ROLE_IDS=role_id_1,role_id_2,role_id_3 diff --git a/.env.production b/.env.production index b8643ed..d3660a6 100644 --- a/.env.production +++ b/.env.production @@ -12,7 +12,7 @@ ADVENT_OF_CODE_CHANNEL_ID=1047623689488830495 SHOWCASE_CHANNEL_ID=1517161718818541658 SHOWCASE_LOG_CHANNEL_ID=1517565847982444634 SHOWCASE_RULES_CHANNEL_ID=1517948527098073158 -ARCHIVED_CATEGORY_ID=837507969859977258 +ARCHIVE_CATEGORY_ID=837507969859977258 # Role IDs (from your dev server) diff --git a/.env.test b/.env.test index d0da5cc..0e25d88 100644 --- a/.env.test +++ b/.env.test @@ -19,7 +19,7 @@ REPEL_LOG_CHANNEL_ID=your-repel-log-channel-id SHOWCASE_CHANNEL_ID=your-showcase-forum-channel-id SHOWCASE_LOG_CHANNEL_ID=your-showcase-log-channel-id SHOWCASE_RULES_CHANNEL_ID=your-showcase-rules-channel-id -ARCHIVED_CATEGORY_ID=your-archived-category-id +ARCHIVE_CATEGORY_ID=your-archived-category-id # Role IDs (from your dev server) REPEL_ROLE_ID=your-repel-role-id diff --git a/src/env.ts b/src/env.ts index b42938f..0a66798 100644 --- a/src/env.ts +++ b/src/env.ts @@ -41,7 +41,7 @@ export const config = { showcase: requireEnv('SHOWCASE_CHANNEL_ID'), showcaseLogs: requireEnv('SHOWCASE_LOG_CHANNEL_ID'), showcaseRules: requireEnv('SHOWCASE_RULES_CHANNEL_ID'), - archiveCategory: requireEnv('ARCHIVED_CATEGORY_ID'), + archiveCategory: requireEnv('ARCHIVE_CATEGORY_ID'), }, onboarding: { channelId: optionalEnv('ONBOARDING_CHANNEL_ID'), From e41152377ed09120db29f196417c38e215b6615e Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 16 Aug 2026 20:58:50 +0300 Subject: [PATCH 7/8] refactor: extract archive channels functions to a util file --- src/features/archive-channels/index.ts | 126 +------------------------ src/features/archive-channels/util.ts | 118 +++++++++++++++++++++++ src/features/ready/index.ts | 4 +- 3 files changed, 125 insertions(+), 123 deletions(-) create mode 100644 src/features/archive-channels/util.ts diff --git a/src/features/archive-channels/index.ts b/src/features/archive-channels/index.ts index ad67de1..26a2049 100644 --- a/src/features/archive-channels/index.ts +++ b/src/features/archive-channels/index.ts @@ -1,24 +1,11 @@ import { createEvent } from '@/common/events/create-event.js'; import { config } from '@/env.js'; +import { Events } from 'discord.js'; import { - ChannelType, - Events, - Guild, - PermissionFlagsBits, - type GuildChannel, -} from 'discord.js'; - -const PUBLIC_PERMISSIONS = [ - PermissionFlagsBits.SendMessages, - PermissionFlagsBits.CreatePublicThreads, - PermissionFlagsBits.CreatePrivateThreads, - PermissionFlagsBits.SendMessagesInThreads, - PermissionFlagsBits.Connect, -]; - -const ARCHIVED_REGEX = /^archived-/; - -const processingChannels = new Set(); + archiveChannel, + processingChannels, + unarchiveChannel, +} from './util.js'; export default createEvent( { @@ -49,106 +36,3 @@ export default createEvent( } } ); - -export async function archiveChannel(channel: GuildChannel) { - const channelName = channel.name; - - const archivedChannelName = channelName.match(ARCHIVED_REGEX) - ? channelName - : `archived-${channelName}`; - - if (archivedChannelName === channelName && hasArchivedPermissions(channel)) { - return; - } - - processingChannels.add(channel.id); - - try { - const renamedChannel = await channel.setName(archivedChannelName); - await setArchivedPermissions(renamedChannel, true); - } catch (error) { - console.error(`Error archiving channel ${channelName}:`, error); - } finally { - processingChannels.delete(channel.id); - } -} - -async function unarchiveChannel(channel: GuildChannel) { - const channelName = channel.name; - - if (!channelName.match(ARCHIVED_REGEX)) { - return; - } - - const newChannelName = channelName.replace(ARCHIVED_REGEX, ''); - - processingChannels.add(channel.id); - - try { - await setArchivedPermissions(channel, false); - await channel.setName(newChannelName); - } catch (error) { - console.error( - `Error unarchiving channel ${channelName}:`, - (error as Error).message - ); - } finally { - processingChannels.delete(channel.id); - } -} - -function hasArchivedPermissions(channel: GuildChannel) { - const everyoneRole = channel.guild.roles.everyone; - const overwrite = channel.permissionOverwrites.cache.get(everyoneRole.id); - - if (!overwrite) { - return false; - } - - return PUBLIC_PERMISSIONS.every((permission) => - overwrite.deny.has(permission) - ); -} - -async function setArchivedPermissions( - channel: GuildChannel, - archived: boolean -) { - await channel.permissionOverwrites.edit( - channel.guild.roles.everyone.id, - { - SendMessages: archived ? false : null, - CreatePublicThreads: archived ? false : null, - CreatePrivateThreads: archived ? false : null, - SendMessagesInThreads: archived ? false : null, - Connect: archived ? false : null, - }, - { - reason: `${archived ? 'Archiving' : 'Unarchiving'} channel ${channel.name}`, - } - ); -} - -export async function ensureArchivedChannelsAreProperlyArchived(guild: Guild) { - const archiveCategory = guild.channels.cache.get( - config.channelIds.archiveCategory - ); - - if (archiveCategory?.type !== ChannelType.GuildCategory) { - console.error( - `❌ Archive category with ID ${config.channelIds.archiveCategory} not found in the guild.` - ); - return; - } - - const archivedChannels = archiveCategory.children.cache; - const results = await Promise.allSettled( - archivedChannels.map(archiveChannel) - ); - - for (const result of results) { - if (result.status === 'rejected') { - console.error('Error archiving channel:', result.reason); - } - } -} diff --git a/src/features/archive-channels/util.ts b/src/features/archive-channels/util.ts new file mode 100644 index 0000000..5007e39 --- /dev/null +++ b/src/features/archive-channels/util.ts @@ -0,0 +1,118 @@ +import { config } from '@/env.js'; +import { + Guild, + ChannelType, + type GuildChannel, + PermissionFlagsBits, +} from 'discord.js'; + +export const processingChannels = new Set(); + +export const ARCHIVED_REGEX = /^archived-/; + +export const PUBLIC_PERMISSIONS = [ + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.CreatePublicThreads, + PermissionFlagsBits.CreatePrivateThreads, + PermissionFlagsBits.SendMessagesInThreads, + PermissionFlagsBits.Connect, +]; + +export async function syncArchiveCategoryChannels(guild: Guild) { + const archiveCategory = guild.channels.cache.get( + config.channelIds.archiveCategory + ); + + if (archiveCategory?.type !== ChannelType.GuildCategory) { + console.error( + `❌ Archive category with ID ${config.channelIds.archiveCategory} not found in the guild.` + ); + return; + } + + const archivedChannels = archiveCategory.children.cache; + const results = await Promise.allSettled( + archivedChannels.map(archiveChannel) + ); + + for (const result of results) { + if (result.status === 'rejected') { + console.error('Error archiving channel:', result.reason); + } + } +} +export async function setArchivedPermissions( + channel: GuildChannel, + archived: boolean +) { + await channel.permissionOverwrites.edit( + channel.guild.roles.everyone.id, + { + SendMessages: archived ? false : null, + CreatePublicThreads: archived ? false : null, + CreatePrivateThreads: archived ? false : null, + SendMessagesInThreads: archived ? false : null, + Connect: archived ? false : null, + }, + { + reason: `${archived ? 'Archiving' : 'Unarchiving'} channel ${channel.name}`, + } + ); +} +export function hasArchivedPermissions(channel: GuildChannel) { + const everyoneRole = channel.guild.roles.everyone; + const overwrite = channel.permissionOverwrites.cache.get(everyoneRole.id); + + if (!overwrite) { + return false; + } + + return PUBLIC_PERMISSIONS.every((permission) => + overwrite.deny.has(permission) + ); +} +export async function unarchiveChannel(channel: GuildChannel) { + const channelName = channel.name; + + if (!channelName.match(ARCHIVED_REGEX)) { + return; + } + + const newChannelName = channelName.replace(ARCHIVED_REGEX, ''); + + processingChannels.add(channel.id); + + try { + await setArchivedPermissions(channel, false); + await channel.setName(newChannelName); + } catch (error) { + console.error( + `Error unarchiving channel ${channelName}:`, + (error as Error).message + ); + } finally { + processingChannels.delete(channel.id); + } +} +export async function archiveChannel(channel: GuildChannel) { + const channelName = channel.name; + + const archivedChannelName = channelName.match(ARCHIVED_REGEX) + ? channelName + : `archived-${channelName}`; + + if (archivedChannelName === channelName && hasArchivedPermissions(channel)) { + return; + } + + processingChannels.add(channel.id); + + try { + const renamedChannel = await channel.setName(archivedChannelName); + await setArchivedPermissions(renamedChannel, true); + } catch (error) { + console.error(`Error archiving channel ${channelName}:`, error); + } finally { + processingChannels.delete(channel.id); + } +} diff --git a/src/features/ready/index.ts b/src/features/ready/index.ts index fa97912..95c365c 100644 --- a/src/features/ready/index.ts +++ b/src/features/ready/index.ts @@ -5,7 +5,7 @@ import { initializeAdventScheduler } from '@/util/advent-scheduler.js'; import { fetchAndCachePublicChannelsMessages } from '@/util/cache.js'; import { syncGuidesToChannel } from '@/util/post-guides.js'; import { leaveIfNotAllowedServer } from '@/util/server-guard.js'; -import { ensureArchivedChannelsAreProperlyArchived } from '../archive-channels/index.js'; +import { syncArchiveCategoryChannels } from '../archive-channels/util.js'; export const readyEvent = createEvent( { @@ -64,7 +64,7 @@ export const readyEvent = createEvent( // Make sure all channels in the archived category are properly archived on startup try { - await ensureArchivedChannelsAreProperlyArchived(guild); + await syncArchiveCategoryChannels(guild); } catch (error) { console.error( '❌ Failed to ensure archived channels are properly archived:', From 6739ff92de361a0b5d81969cec419592c7157954 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 16 Aug 2026 21:07:23 +0300 Subject: [PATCH 8/8] refactor: improve error handling in archive channels util --- src/features/archive-channels/util.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/features/archive-channels/util.ts b/src/features/archive-channels/util.ts index 5007e39..9d2939f 100644 --- a/src/features/archive-channels/util.ts +++ b/src/features/archive-channels/util.ts @@ -24,10 +24,9 @@ export async function syncArchiveCategoryChannels(guild: Guild) { ); if (archiveCategory?.type !== ChannelType.GuildCategory) { - console.error( - `❌ Archive category with ID ${config.channelIds.archiveCategory} not found in the guild.` + throw new Error( + `Archive category with ID ${config.channelIds.archiveCategory} not found in the guild.` ); - return; } const archivedChannels = archiveCategory.children.cache; @@ -35,11 +34,20 @@ export async function syncArchiveCategoryChannels(guild: Guild) { archivedChannels.map(archiveChannel) ); + const failedReasons = []; for (const result of results) { if (result.status === 'rejected') { - console.error('Error archiving channel:', result.reason); + failedReasons.push(result.reason); } } + + if (failedReasons.length > 0) { + const errorMessages = failedReasons + .map((reason) => reason.message || reason) + .join('; '); + console.error(`Failed to archive some channels: ${errorMessages}`); + throw new Error(`Failed to archive some channels: ${errorMessages}`); + } } export async function setArchivedPermissions( channel: GuildChannel, @@ -111,7 +119,10 @@ export async function archiveChannel(channel: GuildChannel) { const renamedChannel = await channel.setName(archivedChannelName); await setArchivedPermissions(renamedChannel, true); } catch (error) { - console.error(`Error archiving channel ${channelName}:`, error); + // console.error(`Error archiving channel ${channelName}:`, error); + throw new Error( + `Error archiving channel ${channelName}: ${(error as Error).message}` + ); } finally { processingChannels.delete(channel.id); }