diff --git a/.env.example b/.env.example index be18af9..bae1c1c 100644 --- a/.env.example +++ b/.env.example @@ -9,10 +9,12 @@ SERVER_ID=your_server_id_here 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 # Role IDs (REQUIRED) MODERATORS_ROLE_IDS=role_id_1,role_id_2,role_id_3 REPEL_ROLE_ID=your_repel_role_id_here +ONBOARDING_ROLE_ID=onboarding_role_id_here # Optional Role IDs # ROLE_A_ID=optional_role_a_id @@ -24,3 +26,6 @@ REPEL_ROLE_ID=your_repel_role_id_here # Docker deployments should use /app/data for persistence GUIDES_TRACKER_PATH=guides-tracker.json ADVENT_OF_CODE_TRACKER_PATH=advent-of-code-tracker.json + + + diff --git a/src/commands/onboarding/component.ts b/src/commands/onboarding/component.ts new file mode 100644 index 0000000..4245246 --- /dev/null +++ b/src/commands/onboarding/component.ts @@ -0,0 +1,22 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ContainerBuilder, + type MessageActionRowComponentBuilder, +} from 'discord.js'; + +// Exported at the bottom after all child components are added +const containerComponent = new ContainerBuilder(); + +const actionRowComponent = new ActionRowBuilder(); + +const buttonComponent = new ButtonBuilder() + .setCustomId('onboarding_add_role') + .setLabel('Add role') + .setStyle(ButtonStyle.Primary); + +actionRowComponent.addComponents(buttonComponent); +containerComponent.addActionRowComponents(actionRowComponent); + +export { containerComponent }; diff --git a/src/commands/onboarding/index.ts b/src/commands/onboarding/index.ts new file mode 100644 index 0000000..9841fdf --- /dev/null +++ b/src/commands/onboarding/index.ts @@ -0,0 +1,67 @@ +import { ApplicationCommandType, MessageFlags } from 'discord.js'; +import { config } from '../../env.js'; +import { addRoleToUser } from '../../util/addRoleToUser.js'; +import { createCommand } from '../../util/commands.js'; +import { containerComponent } from './component.js'; + +export const onboardingCommand = createCommand({ + data: { + name: 'onboarding', + description: 'Manage onboarding settings', + type: ApplicationCommandType.ChatInput, + }, + execute: async (interaction) => { + const guild = interaction.guild; + if (!guild) { + await interaction.reply({ + content: 'This command can only be used in a server.', + flags: MessageFlags.Ephemeral, + }); + return; + } + const onboardingRole = guild.roles.cache.get(config.onboarding.roleId); + if (!onboardingRole) { + await interaction.reply({ + content: 'Onboarding role not found. Please check the configuration.', + flags: MessageFlags.Ephemeral, + }); + return; + } + const onboardingChannel = guild.channels.cache.get(config.onboarding.channelId); + if (!onboardingChannel || !onboardingChannel.isSendable()) { + await interaction.reply({ + content: + 'Onboarding channel not found or is not a text channel. Please check the configuration.', + flags: MessageFlags.Ephemeral, + }); + return; + } + + const onboardingMessage = await interaction.reply({ + components: [containerComponent], + flags: MessageFlags.IsComponentsV2, + }); + + const collector = onboardingMessage.createMessageComponentCollector({}); + + collector.on('collect', async (componentInteraction) => { + if (componentInteraction.customId === 'onboarding_add_role') { + try { + const member = await guild.members.fetch(componentInteraction.user.id); + await addRoleToUser(member, onboardingRole); + + await componentInteraction.reply({ + content: `You have been given the ${onboardingRole.name} role!`, + flags: MessageFlags.Ephemeral, + }); + } catch (error) { + await componentInteraction.reply({ + content: `Failed to add role. Please contact an administrator.`, + flags: MessageFlags.Ephemeral, + }); + console.error('Error adding role:\n', error); + } + } + }); + }, +}); diff --git a/src/env.ts b/src/env.ts index ace8485..12a93cd 100644 --- a/src/env.ts +++ b/src/env.ts @@ -38,6 +38,17 @@ export const config = { guides: requireEnv('GUIDES_CHANNEL_ID'), adventOfCode: requireEnv('ADVENT_OF_CODE_CHANNEL_ID'), }, + onboarding: { + channelId: optionalEnv('ONBOARDING_CHANNEL_ID'), + roleId: optionalEnv('ONBOARDING_ROLE_ID'), + }, + // Add more config sections as needed: + // database: { + // url: requireEnv('DATABASE_URL'), + // }, + // api: { + // openaiKey: optionalEnv('OPENAI_API_KEY'), + // }, }; export type Config = typeof config; diff --git a/src/util/addRoleToUser.ts b/src/util/addRoleToUser.ts new file mode 100644 index 0000000..e4cdf02 --- /dev/null +++ b/src/util/addRoleToUser.ts @@ -0,0 +1,14 @@ +import type { GuildMember, Role } from 'discord.js'; + +export async function addRoleToUser(member: GuildMember, role: Role): Promise { + const hasRole = member.roles.cache.has(role.id); + if (!hasRole) { + try { + await member.roles.add(role); + } catch (error) { + throw new Error( + `Failed to add role "${role.name}" to user "${member.user.username}": ${error instanceof Error ? error.message : String(error)}` + ); + } + } +}