Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
72 changes: 72 additions & 0 deletions src/constants/channels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type {
CategoryChannel,
ForumChannel,
Guild,
TextChannel,
} from 'discord.js';
import { ChannelType } from 'discord.js';
import { config } from '@/env.js';

export type ChannelKey = keyof typeof config.channelIds;

type ChannelTypeMap = {
repelLogs: TextChannel;
guides: TextChannel;
adventOfCode: ForumChannel;
showcase: ForumChannel;
showcaseLogs: TextChannel;
showcaseRules: TextChannel;
spamDetection: TextChannel;
archiveCategory: CategoryChannel;
};

const EXPECTED_DISCORD_TYPE: Record<ChannelKey, ChannelType> = {
repelLogs: ChannelType.GuildText,
guides: ChannelType.GuildText,
adventOfCode: ChannelType.GuildForum,
showcase: ChannelType.GuildForum,
showcaseLogs: ChannelType.GuildText,
showcaseRules: ChannelType.GuildText,
spamDetection: ChannelType.GuildText,
archiveCategory: ChannelType.GuildCategory,
};

const resolveChannel = <Key extends ChannelKey>(
guild: Guild,
key: Key
): ChannelTypeMap[Key] => {
const channelId = config.channelIds[key];
const channel = guild.channels.cache.get(channelId);

if (!channel) {
throw new Error(
`Channel with ID ${channelId} (key: ${key}) not found in the guild.`
);
}

const expectedType = EXPECTED_DISCORD_TYPE[key];
if (channel.type !== expectedType) {
throw new Error(
`Channel "${key}" (${channelId}) has type ${ChannelType[channel.type]}, expected ${ChannelType[expectedType]}.`
);
}

return channel as ChannelTypeMap[Key];
};

export const SERVER_CHANNELS = {} as {
[Key in ChannelKey]: ChannelTypeMap[Key];
};

const assignResolvedChannel = <Key extends ChannelKey>(
key: Key,
channel: ChannelTypeMap[Key]
): void => {
SERVER_CHANNELS[key] = channel;
};

export const resolveChannels = (guild: Guild): void => {
(Object.keys(config.channelIds) as ChannelKey[]).forEach((key) => {
assignResolvedChannel(key, resolveChannel(guild, key));
});
};
17 changes: 3 additions & 14 deletions src/features/archive-channels/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { config } from '@/env.js';
import { SERVER_CHANNELS } from '@/constants/channels.js';
import {
Guild,
ChannelType,
type GuildChannel,
PermissionFlagsBits,
Expand All @@ -18,18 +17,8 @@ export const PUBLIC_PERMISSIONS = [
PermissionFlagsBits.Connect,
];

export async function syncArchiveCategoryChannels(guild: Guild) {
const archiveCategory = guild.channels.cache.get(
config.channelIds.archiveCategory
);

if (archiveCategory?.type !== ChannelType.GuildCategory) {
throw new Error(
`Archive category with ID ${config.channelIds.archiveCategory} not found in the guild.`
);
}

const archivedChannels = archiveCategory.children.cache;
export async function syncArchiveCategoryChannels() {
const archivedChannels = SERVER_CHANNELS.archiveCategory.children.cache;
const results = await Promise.allSettled(
archivedChannels.map(archiveChannel)
);
Expand Down
7 changes: 2 additions & 5 deletions src/features/moderation/repel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { config } from '../../env.js';
import { buildCommandString } from '../../util/build-command-string.js';
import { getPublicChannels } from '../../util/channel.js';
import { logToChannel } from '../../util/channel-logging.js';
import { SERVER_CHANNELS } from '@/constants/channels.js';

const DEFAULT_LOOK_BACK_MS = 10 * MINUTE;
const DEFAULT_TIMEOUT_DURATION_MS = 1 * HOUR;
Expand Down Expand Up @@ -370,17 +371,13 @@ const logRepelAction = async ({
const mentionText = modMessage
? `${config.roleIds.moderators.map((id) => `<@&${id}>`).join(' ')} - ${modMessage}`
: undefined;
const channel = interaction.client.channels.cache.get(
config.channelIds.repelLogs
) as TextChannel;

const embed =
failedChannelsEmbed !== null
? [commandEmbed, resultEmbed, failedChannelsEmbed]
: [commandEmbed, resultEmbed];

await logToChannel({
channel,
channel: SERVER_CHANNELS.repelLogs,
content: {
type: 'embed',
embed,
Expand Down
38 changes: 20 additions & 18 deletions src/features/ready/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Events } from 'discord.js';
import { createEvent } from '@/common/events/create-event.js';
import { resolveChannels } from '@/constants/channels.js';
import { config } from '@/env.js';
import { initializeAdventScheduler } from '@/util/advent-scheduler.js';
import { fetchAndCachePublicChannelsMessages } from '@/util/channel-prefetch.js';
Expand Down Expand Up @@ -31,28 +32,29 @@ export const readyEvent = createEvent(
process.exit(1);
}

resolveChannels(guild);

if (config.fetchAndSyncMessages) {
await fetchAndCachePublicChannelsMessages(guild, true);

// Sync guides to channel
try {
console.log(
`🔄 Starting guide sync to channel ${config.channelIds.guides}...`
);
await syncGuidesToChannel(client, config.channelIds.guides);
} catch (error) {
if (error && typeof error === 'object' && 'code' in error) {
const discordError = error as { code: number; message?: string };
if (discordError.code === 50001) {
console.warn(
'⚠️ Bot does not have access to the guides channel. Please check bot permissions and channel ID.'
);
} else {
console.error('❌ Failed to sync guides:', error);
}
}
// Sync guides to channel
try {
console.log(
`🔄 Starting guide sync to channel ${config.channelIds.guides}...`
);
await syncGuidesToChannel(client, config.channelIds.guides);
} catch (error) {
if (error && typeof error === 'object' && 'code' in error) {
const discordError = error as { code: number; message?: string };
if (discordError.code === 50001) {
console.warn(
'⚠️ Bot does not have access to the guides channel. Please check bot permissions and channel ID.'
);
} else {
console.error('❌ Failed to sync guides:', error);
}
} else {
console.error('❌ Failed to sync guides:', error);
}
}

Expand All @@ -65,7 +67,7 @@ export const readyEvent = createEvent(

// Make sure all channels in the archived category are properly archived on startup
try {
await syncArchiveCategoryChannels(guild);
await syncArchiveCategoryChannels();
} catch (error) {
console.error(
'❌ Failed to ensure archived channels are properly archived:',
Expand Down
15 changes: 3 additions & 12 deletions src/features/report-message/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createMessageContextMenuCommand } from '@/common/commands/create-commands.js';
import { config } from '@/env.js';
import { ChannelType, Colors, EmbedBuilder, MessageFlags } from 'discord.js';
import { SERVER_CHANNELS } from '@/constants/channels.js';
import { Colors, EmbedBuilder, MessageFlags } from 'discord.js';

export const reportMessage = createMessageContextMenuCommand({
data: {
Expand All @@ -22,17 +22,8 @@ export const reportMessage = createMessageContextMenuCommand({

const targetMessage = interaction.targetMessage;
const reporter = interaction.user;
const channelId = config.channelIds.spamDetection;
const channel = guild.channels.cache.get(channelId);

try {
if (!channel || channel.type !== ChannelType.GuildText) {
await interaction.editReply({
content: 'Moderator channel not found or is not a text channel.',
});
return;
}

const jumpLink = targetMessage.url;
const authorTag = targetMessage.author.tag ?? 'Unknown';
const authorId = targetMessage.author.id ?? 'Unknown';
Expand All @@ -55,7 +46,7 @@ export const reportMessage = createMessageContextMenuCommand({
{ name: 'Linked User', value: `<@${authorId}>`, inline: true }
);

await channel.send({ embeds: [embed] });
await SERVER_CHANNELS.spamDetection.send({ embeds: [embed] });

await interaction.editReply({
content: 'Thanks. The message was reported to moderators.',
Expand Down
39 changes: 5 additions & 34 deletions src/features/showcase/create-showcase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
ButtonBuilder,
type ButtonInteraction,
ButtonStyle,
ChannelType,
type ChatInputCommandInteraction,
Colors,
ContainerBuilder,
Expand All @@ -19,36 +18,20 @@ import {
type ModalSubmitInteraction,
registerModalSubmitInteraction,
} from '@/common/interactions/modal-interaction.js';
import { config } from '@/env.js';
import { logToChannel } from '@/util/channel-logging.js';
import { customId } from '@/util/custom-id.js';
import { deleteShowcase } from './delete-showcase.js';
import { editShowcaseInteraction } from './edit-showcase.js';
import {
buildShowcaseModal,
createShowcaseMessageContent,
getShowcaseLogChannel,
} from './util.js';
import { buildShowcaseModal, createShowcaseMessageContent } from './util.js';
import { SERVER_CHANNELS } from '@/constants/channels.js';

export const showModal = async (
interaction: ButtonInteraction | ChatInputCommandInteraction
) => {
try {
const channel = interaction.guild?.channels.cache.get(
config.channelIds.showcase
);
if (channel === undefined || channel.type !== ChannelType.GuildForum) {
await interaction.reply({
content:
'Showcase channel is not properly configured. Please contact an administrator.',
flags: MessageFlags.Ephemeral,
});
return;
}

const modal = buildShowcaseModal({
id: customId('showcase', interaction.user.id),
tags: channel.availableTags,
tags: SERVER_CHANNELS.showcase.availableTags,
});

await interaction.showModal(modal);
Expand Down Expand Up @@ -88,19 +71,8 @@ const modalHandler: ModalSubmitInteraction = {
const projectTags = interaction.fields.getStringSelectValues('projectTags');
const projectMedia = interaction.fields.getUploadedFiles('projectMedia');

const channel = interaction.guild?.channels.cache.get(
config.channelIds.showcase
);
if (channel === undefined || channel.type !== ChannelType.GuildForum) {
await interaction.editReply({
content:
'Showcase channel is not properly configured. Please contact an administrator.',
});
return;
}

try {
const thread = await channel.threads.create({
const thread = await SERVER_CHANNELS.showcase.threads.create({
name: projectName,
appliedTags: projectTags,
message: {
Expand Down Expand Up @@ -142,7 +114,6 @@ const modalHandler: ModalSubmitInteraction = {
});

try {
const logChannel = getShowcaseLogChannel(interaction.guild);
const author = {
name: interaction.user.tag,
iconURL: interaction.user.displayAvatarURL(),
Expand All @@ -164,7 +135,7 @@ const modalHandler: ModalSubmitInteraction = {
.setTimestamp();

await logToChannel({
channel: logChannel,
channel: SERVER_CHANNELS.showcaseLogs,
content: { type: 'embed', embed },
silent: true,
});
Expand Down
5 changes: 2 additions & 3 deletions src/features/showcase/delete-showcase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ButtonSubmitInteraction } from '@/common/interactions/button-inter
import { logToChannel } from '@/util/channel-logging.js';
import { parseCustomId } from '@/util/custom-id.js';
import { isUserInServer, isUserModerator } from '@/util/member.js';
import { getShowcaseLogChannel } from './util.js';
import { SERVER_CHANNELS } from '@/constants/channels.js';

export const deleteShowcase: ButtonSubmitInteraction = {
commandName: 'delete_showcase',
Expand Down Expand Up @@ -61,9 +61,8 @@ export const deleteShowcase: ButtonSubmitInteraction = {

const projectName = forumPost.name;
await interaction.channel?.delete();
const logChannel = getShowcaseLogChannel(interaction.guild);
await logToChannel({
channel: logChannel,
channel: SERVER_CHANNELS.showcaseLogs,
content: {
type: 'embed',
embed: new EmbedBuilder()
Expand Down
5 changes: 2 additions & 3 deletions src/features/showcase/edit-showcase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import {
buildShowcaseModal,
createShowcaseMessageContent,
getAttachmentsCount,
getShowcaseLogChannel,
parseShowcaseMessage,
resolveTagNames,
} from './util.js';
import { SERVER_CHANNELS } from '@/constants/channels.js';

export const editShowcaseInteraction: ButtonSubmitInteraction = {
commandName: 'edit_showcase',
Expand Down Expand Up @@ -266,7 +266,6 @@ const modalHandler: ModalSubmitInteraction = {

if (changes.length > 0) {
try {
const logChannel = getShowcaseLogChannel(interaction.guild);
const author = {
name: interaction.user.tag,
iconURL: interaction.user.displayAvatarURL(),
Expand Down Expand Up @@ -302,7 +301,7 @@ const modalHandler: ModalSubmitInteraction = {
.setColor(Colors.Orange)
.setTimestamp();

await logChannel.send({
await SERVER_CHANNELS.showcaseLogs.send({
embeds: [embed],
allowedMentions: { parse: [] },
});
Expand Down
13 changes: 3 additions & 10 deletions src/features/showcase/send-pinned-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
PermissionsBitField,
} from 'discord.js';
import { createSlashCommand } from '@/common/commands/create-commands.js';
import { config } from '@/env.js';
import { SERVER_CHANNELS } from '@/constants/channels.js';

export const sendShowcasePinnedMessage = createSlashCommand({
data: {
Expand All @@ -20,15 +20,8 @@ export const sendShowcasePinnedMessage = createSlashCommand({
},
execute: async (interaction) => {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const showcaseChannel = interaction.guild?.channels.cache.get(
config.channelIds.showcaseRules
);
if (showcaseChannel === undefined || !showcaseChannel.isTextBased()) {
await interaction.editReply({
content: 'Showcase channel not found or is not a forum channel.',
});
return;
}

const showcaseChannel = SERVER_CHANNELS.showcaseRules;

const guideLines = [
'Welcome to the Showcase channel! Please read the rules and guidelines before posting your content. Make sure to follow the format and include all necessary information. Happy sharing!',
Expand Down
Loading
Loading