Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
ARCHIVE_CATEGORY_ID=archive_category_id_here

# Role IDs (REQUIRED)
MODERATORS_ROLE_IDS=role_id_1,role_id_2,role_id_3
Expand Down
1 change: 1 addition & 0 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ADVENT_OF_CODE_CHANNEL_ID=1047623689488830495
SHOWCASE_CHANNEL_ID=1517161718818541658
SHOWCASE_LOG_CHANNEL_ID=1517565847982444634
SHOWCASE_RULES_CHANNEL_ID=1517948527098073158
ARCHIVE_CATEGORY_ID=837507969859977258


# Role IDs (from your dev server)
Expand Down
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
ARCHIVE_CATEGORY_ID=your-archived-category-id

# Role IDs (from your dev server)
REPEL_ROLE_ID=your-repel-role-id
Expand Down
2 changes: 2 additions & 0 deletions src/common/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
1 change: 1 addition & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const config = {
showcase: requireEnv('SHOWCASE_CHANNEL_ID'),
showcaseLogs: requireEnv('SHOWCASE_LOG_CHANNEL_ID'),
showcaseRules: requireEnv('SHOWCASE_RULES_CHANNEL_ID'),
archiveCategory: requireEnv('ARCHIVE_CATEGORY_ID'),
},
onboarding: {
channelId: optionalEnv('ONBOARDING_CHANNEL_ID'),
Expand Down
154 changes: 154 additions & 0 deletions src/features/archive-channels/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { createEvent } from '@/common/events/create-event.js';
import { config } from '@/env.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<string>();
Comment thread
wiktoriavh marked this conversation as resolved.
Outdated

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.archiveCategory &&
oldChannel.parentId !== config.channelIds.archiveCategory
) {
return;
}

// Prevent deadlocks from nested ChannelUpdate events caused by our own edits
if (processingChannels.has(newChannel.id)) {
return;
}

if (newChannel.parentId === config.channelIds.archiveCategory) {
await archiveChannel(newChannel);
} else {
await unarchiveChannel(newChannel);
}
}
);

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}`,
}
);
}
Comment thread
hmd-ali marked this conversation as resolved.
Outdated

export async function ensureArchivedChannelsAreProperlyArchived(guild: Guild) {
Comment thread
hmd-ali marked this conversation as resolved.
Outdated
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;
Comment thread
hmd-ali marked this conversation as resolved.
Outdated
}

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);
}
}
Comment thread
hmd-ali marked this conversation as resolved.
Outdated
}
25 changes: 21 additions & 4 deletions src/features/ready/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +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';

export const readyEvent = createEvent(
{
Expand All @@ -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 {
Expand Down Expand Up @@ -54,5 +61,15 @@ 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 {
await ensureArchivedChannelsAreProperlyArchived(guild);
} catch (error) {
console.error(
'❌ Failed to ensure archived channels are properly archived:',
error
);
}
Comment thread
hmd-ali marked this conversation as resolved.
}
Comment thread
hmd-ali marked this conversation as resolved.
);
Loading