From e76ae3de999881bc51788b550372c00d6fc20bc0 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Mon, 3 Nov 2025 23:06:44 +0200 Subject: [PATCH 01/12] =?UTF-8?q?=F0=9F=94=A8=20refactor:=20run=20check=20?= =?UTF-8?q?script=20(organize=20imports)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/moderation/repel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/moderation/repel.ts b/src/commands/moderation/repel.ts index 4781b3f..667bb2b 100644 --- a/src/commands/moderation/repel.ts +++ b/src/commands/moderation/repel.ts @@ -14,8 +14,8 @@ import { } from 'discord.js'; import { HOUR, MINUTE, timeToString } from '../../constants/time.js'; import { config } from '../../env.js'; -import { logToChannel } from '../../util/channel-logging.js'; import { getPublicChannels } from '../../util/channel.js'; +import { logToChannel } from '../../util/channel-logging.js'; import { buildCommandString, createCommand } from '../../util/commands.js'; const DEFAULT_LOOK_BACK_MS = 10 * MINUTE; From fd1e4ecfb17211605a6d8090f4ebf494ef8a3fad Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Tue, 4 Nov 2025 13:35:02 +0200 Subject: [PATCH 02/12] =?UTF-8?q?=F0=9F=8C=9F=20feat:=20add=20onboarding?= =?UTF-8?q?=20channelId=20and=20roleId=20to=20env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 3 +++ src/env.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/.env.example b/.env.example index 6323e1b..1430c84 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,6 @@ MODERATORS_ROLE_IDS= # Comma separated list of role IDs that are Moderators(Mods REPEL_LOG_CHANNEL_ID= # Channel ID where the bot will log repel actions REPEL_ROLE_ID= # Role ID assigned to users who can use the repel command GUIDES_CHANNEL_ID="" # The ID of the channel where guides will be posted + +ONBOARDING_CHANNEL_ID= # Channel ID where onboarding messages will be sent +ONBOARDING_ROLE_ID= # Role ID assigned to new members upon onboarding diff --git a/src/env.ts b/src/env.ts index cc90dea..36eb214 100644 --- a/src/env.ts +++ b/src/env.ts @@ -33,6 +33,10 @@ export const config = { channelId: requireEnv('GUIDES_CHANNEL_ID'), trackerPath: optionalEnv('GUIDES_TRACKER_PATH'), }, + onboarding: { + channelId: requireEnv('ONBOARDING_CHANNEL_ID'), + roleId: requireEnv('ONBOARDING_ROLE_ID'), + }, // Add more config sections as needed: // database: { // url: requireEnv('DATABASE_URL'), From 288f0c1d8afdb6f440b91b0c481b4fe4c7007b42 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Tue, 4 Nov 2025 13:35:29 +0200 Subject: [PATCH 03/12] =?UTF-8?q?=F0=9F=8C=9F=20feat:=20add=20onboarding?= =?UTF-8?q?=20component=20with=20button=20for=20role=20addition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/onboarding/component.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/commands/onboarding/component.ts diff --git a/src/commands/onboarding/component.ts b/src/commands/onboarding/component.ts new file mode 100644 index 0000000..ed4e7e0 --- /dev/null +++ b/src/commands/onboarding/component.ts @@ -0,0 +1,21 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ContainerBuilder, + type MessageActionRowComponentBuilder, +} from 'discord.js'; + +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 }; From e3f7cecda57616925ae18f82091fc40a96926ca7 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Tue, 4 Nov 2025 13:36:06 +0200 Subject: [PATCH 04/12] =?UTF-8?q?=F0=9F=8C=9F=20feat:=20add=20onboarding?= =?UTF-8?q?=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/index.ts | 11 +++++- src/commands/onboarding/index.ts | 65 ++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/commands/onboarding/index.ts diff --git a/src/commands/index.ts b/src/commands/index.ts index 3bb312e..dee95e9 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -2,12 +2,21 @@ import { docsCommands } from './docs/index.js'; import { guidesCommand } from './guides/index.js'; import cacheMessages from './moderation/cache-messages.js'; import { repelCommand } from './moderation/repel.js'; +import { onboardingCommand } from './onboarding/index.js'; import { pingCommand } from './ping.js'; import { tipsCommands } from './tips/index.js'; import type { Command } from './types.js'; export const commands = new Map( - [pingCommand, guidesCommand, docsCommands, tipsCommands, repelCommand, cacheMessages] + [ + pingCommand, + guidesCommand, + docsCommands, + tipsCommands, + repelCommand, + cacheMessages, + onboardingCommand, + ] .flat() .map((cmd) => [cmd.data.name, cmd]) ); diff --git a/src/commands/onboarding/index.ts b/src/commands/onboarding/index.ts new file mode 100644 index 0000000..d433dca --- /dev/null +++ b/src/commands/onboarding/index.ts @@ -0,0 +1,65 @@ +import { ApplicationCommandType, MessageFlags } from 'discord.js'; +import { config } from '../../env.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') { + const member = await guild.members.fetch(componentInteraction.user.id); + const hasRole = member.roles.cache.has(onboardingRole.id); + if (hasRole) { + await componentInteraction.reply({ + content: `You already have the ${onboardingRole.name} role.`, + flags: MessageFlags.Ephemeral, + }); + } else { + await member.roles.add(onboardingRole); + await componentInteraction.reply({ + content: `You have been given the ${onboardingRole.name} role!`, + flags: MessageFlags.Ephemeral, + }); + } + } + }); + }, +}); From 896d89df31c64ea9841cc6bd05238b4678602aa7 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Tue, 4 Nov 2025 13:38:02 +0200 Subject: [PATCH 05/12] =?UTF-8?q?=F0=9F=A4=96=20ci:=20add=20onboarding=20c?= =?UTF-8?q?hannelId=20and=20roleId=20to=20deployment=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 58e6a74..ebbdf8c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -47,6 +47,8 @@ jobs: REPEL_LOG_CHANNEL_ID=${{ secrets.REPEL_LOG_CHANNEL_ID }} REPEL_ROLE_ID=${{ secrets.REPEL_ROLE_ID }} MODERATORS_ROLE_IDS=${{ secrets.MODERATORS_ROLE_IDS }} + ONBOARDING_CHANNEL_ID=${{ secrets.ONBOARDING_CHANNEL_ID }} + ONBOARDING_ROLE_ID=${{ secrets.ONBOARDING_ROLE_ID }} EOF # Stop any existing containers From efef3f917251a0020e032ad7d5cc34dd403ba625 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Tue, 4 Nov 2025 13:48:05 +0200 Subject: [PATCH 06/12] =?UTF-8?q?=F0=9F=8C=9F=20feat:=20update=20web-featu?= =?UTF-8?q?res=20dependency=20to=20version=203.7.0=20and=20add=20'open-clo?= =?UTF-8?q?sed'=20to=20NON=5FBASELINE=5FFEATURES?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- src/commands/docs/utils.ts | 6 +++++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 7d00706..21dcc6e 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "dependencies": { "discord.js": "^14.22.1", "typescript": "^5.9.3", - "web-features": "^3.3.0" + "web-features": "^3.7.0" }, "devDependencies": { "@biomejs/biome": "2.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a35a473..637a258 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^5.9.3 version: 5.9.3 web-features: - specifier: ^3.3.0 - version: 3.5.0 + specifier: ^3.7.0 + version: 3.7.0 devDependencies: '@biomejs/biome': specifier: 2.2.4 @@ -915,8 +915,8 @@ packages: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} - web-features@3.5.0: - resolution: {integrity: sha512-8e59t2hpjCtVpO+bgdGgyrTBN7CWRYx2dt8TqRJLeMmJ3LpjT4XNSoxYykZi0Fuc5qorbKWXPgseD3PHtyQJ5w==} + web-features@3.7.0: + resolution: {integrity: sha512-97Fif4cwt6AuoEZOYfpjHY0SZlV2u+3AuiHqJPxIzzAvKVXErkt3ST+z1ySFuJqS1+7OU8zAR/iFY1a6jnmMsA==} webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -1721,7 +1721,7 @@ snapshots: undici@6.21.3: {} - web-features@3.5.0: {} + web-features@3.7.0: {} webidl-conversions@4.0.2: {} diff --git a/src/commands/docs/utils.ts b/src/commands/docs/utils.ts index 1d55053..fc39a97 100644 --- a/src/commands/docs/utils.ts +++ b/src/commands/docs/utils.ts @@ -92,7 +92,11 @@ export const executeDocCommand = async ( } }; -export const NON_BASELINE_FEATURES = ['numeric-seperators', 'single-color-gradients']; +export const NON_BASELINE_FEATURES = [ + 'numeric-seperators', + 'open-closed', + 'single-color-gradients', +]; export const getBaselineFeatures = ( originalFeatures: Record, nonFeatureKeys: string[] = NON_BASELINE_FEATURES From fe6acae7a9e71bf45375f7e578431881af9de3c0 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Tue, 4 Nov 2025 19:19:22 +0100 Subject: [PATCH 07/12] refactor: use addRoleToUser utility for role assignment --- src/commands/onboarding/index.ts | 15 ++---------- src/util/addRoleToUser.ts | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 src/util/addRoleToUser.ts diff --git a/src/commands/onboarding/index.ts b/src/commands/onboarding/index.ts index d433dca..a993864 100644 --- a/src/commands/onboarding/index.ts +++ b/src/commands/onboarding/index.ts @@ -1,5 +1,6 @@ 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'; @@ -46,19 +47,7 @@ export const onboardingCommand = createCommand({ collector.on('collect', async (componentInteraction) => { if (componentInteraction.customId === 'onboarding_add_role') { const member = await guild.members.fetch(componentInteraction.user.id); - const hasRole = member.roles.cache.has(onboardingRole.id); - if (hasRole) { - await componentInteraction.reply({ - content: `You already have the ${onboardingRole.name} role.`, - flags: MessageFlags.Ephemeral, - }); - } else { - await member.roles.add(onboardingRole); - await componentInteraction.reply({ - content: `You have been given the ${onboardingRole.name} role!`, - flags: MessageFlags.Ephemeral, - }); - } + await addRoleToUser(member, onboardingRole, componentInteraction); } }); }, diff --git a/src/util/addRoleToUser.ts b/src/util/addRoleToUser.ts new file mode 100644 index 0000000..2ee615a --- /dev/null +++ b/src/util/addRoleToUser.ts @@ -0,0 +1,40 @@ +import { + type ButtonInteraction, + type CacheType, + type ChannelSelectMenuInteraction, + type GuildMember, + type MentionableSelectMenuInteraction, + MessageFlags, + type Role, + type RoleSelectMenuInteraction, + type StringSelectMenuInteraction, + type UserSelectMenuInteraction, +} from 'discord.js'; + +type ComponentInteraction = + | ButtonInteraction + | StringSelectMenuInteraction + | UserSelectMenuInteraction + | RoleSelectMenuInteraction + | MentionableSelectMenuInteraction + | ChannelSelectMenuInteraction; + +export async function addRoleToUser( + member: GuildMember, + role: Role, + interaction: ComponentInteraction +) { + const hasRole = member.roles.cache.has(role.id); + if (hasRole) { + await interaction.reply({ + content: `You already have the ${role.name} role.`, + flags: MessageFlags.Ephemeral, + }); + } else { + await member.roles.add(role); + await interaction.reply({ + content: `You have been given the ${role.name} role!`, + flags: MessageFlags.Ephemeral, + }); + } +} From c33fba6f80b996dbae3f25f34461a6159e82cc91 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Tue, 4 Nov 2025 19:23:32 +0100 Subject: [PATCH 08/12] chore: add comment to clarify that export is later --- src/commands/onboarding/component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/onboarding/component.ts b/src/commands/onboarding/component.ts index ed4e7e0..4245246 100644 --- a/src/commands/onboarding/component.ts +++ b/src/commands/onboarding/component.ts @@ -6,6 +6,7 @@ import { type MessageActionRowComponentBuilder, } from 'discord.js'; +// Exported at the bottom after all child components are added const containerComponent = new ContainerBuilder(); const actionRowComponent = new ActionRowBuilder(); From 45ae96c3c3055669d3d8c065d5348e9294162aa7 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Wed, 5 Nov 2025 18:23:59 +0100 Subject: [PATCH 09/12] chore: should not be part of the curren active commands --- src/commands/index.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/commands/index.ts b/src/commands/index.ts index dee95e9..3bb312e 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -2,21 +2,12 @@ import { docsCommands } from './docs/index.js'; import { guidesCommand } from './guides/index.js'; import cacheMessages from './moderation/cache-messages.js'; import { repelCommand } from './moderation/repel.js'; -import { onboardingCommand } from './onboarding/index.js'; import { pingCommand } from './ping.js'; import { tipsCommands } from './tips/index.js'; import type { Command } from './types.js'; export const commands = new Map( - [ - pingCommand, - guidesCommand, - docsCommands, - tipsCommands, - repelCommand, - cacheMessages, - onboardingCommand, - ] + [pingCommand, guidesCommand, docsCommands, tipsCommands, repelCommand, cacheMessages] .flat() .map((cmd) => [cmd.data.name, cmd]) ); From 00b10df8eb0abe50690b459ccc30679776cb01f2 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Wed, 5 Nov 2025 18:57:00 +0100 Subject: [PATCH 10/12] fix: improve error handling in role assignment --- src/commands/onboarding/index.ts | 17 ++++++++++-- src/util/addRoleToUser.ts | 46 +++++++------------------------- 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/src/commands/onboarding/index.ts b/src/commands/onboarding/index.ts index a993864..9841fdf 100644 --- a/src/commands/onboarding/index.ts +++ b/src/commands/onboarding/index.ts @@ -46,8 +46,21 @@ export const onboardingCommand = createCommand({ collector.on('collect', async (componentInteraction) => { if (componentInteraction.customId === 'onboarding_add_role') { - const member = await guild.members.fetch(componentInteraction.user.id); - await addRoleToUser(member, onboardingRole, componentInteraction); + 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/util/addRoleToUser.ts b/src/util/addRoleToUser.ts index 2ee615a..e4cdf02 100644 --- a/src/util/addRoleToUser.ts +++ b/src/util/addRoleToUser.ts @@ -1,40 +1,14 @@ -import { - type ButtonInteraction, - type CacheType, - type ChannelSelectMenuInteraction, - type GuildMember, - type MentionableSelectMenuInteraction, - MessageFlags, - type Role, - type RoleSelectMenuInteraction, - type StringSelectMenuInteraction, - type UserSelectMenuInteraction, -} from 'discord.js'; +import type { GuildMember, Role } from 'discord.js'; -type ComponentInteraction = - | ButtonInteraction - | StringSelectMenuInteraction - | UserSelectMenuInteraction - | RoleSelectMenuInteraction - | MentionableSelectMenuInteraction - | ChannelSelectMenuInteraction; - -export async function addRoleToUser( - member: GuildMember, - role: Role, - interaction: ComponentInteraction -) { +export async function addRoleToUser(member: GuildMember, role: Role): Promise { const hasRole = member.roles.cache.has(role.id); - if (hasRole) { - await interaction.reply({ - content: `You already have the ${role.name} role.`, - flags: MessageFlags.Ephemeral, - }); - } else { - await member.roles.add(role); - await interaction.reply({ - content: `You have been given the ${role.name} role!`, - flags: MessageFlags.Ephemeral, - }); + 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)}` + ); + } } } From 7288b0296aca34a0e6370ca3e6820edd1290758c Mon Sep 17 00:00:00 2001 From: Cake Date: Wed, 12 Nov 2025 21:42:35 +0200 Subject: [PATCH 11/12] Change onboarding channel and role IDs to optional --- src/env.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/env.ts b/src/env.ts index 739132e..ca3e1f2 100644 --- a/src/env.ts +++ b/src/env.ts @@ -37,8 +37,8 @@ export const config = { guides: requireEnv('GUIDES_CHANNEL_ID'), }, onboarding: { - channelId: requireEnv('ONBOARDING_CHANNEL_ID'), - roleId: requireEnv('ONBOARDING_ROLE_ID'), + channelId: optionalEnv('ONBOARDING_CHANNEL_ID'), + roleId: optionalEnv('ONBOARDING_ROLE_ID'), }, // Add more config sections as needed: // database: { From 526e3d91a68a56c83ed4e2196e9a938f6ac49afd Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Thu, 13 Nov 2025 02:46:55 +0200 Subject: [PATCH 12/12] remove duplicate vars in example env --- .env.example | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 4ed8885..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 @@ -22,16 +24,6 @@ REPEL_ROLE_ID=your_repel_role_id_here # Data Persistence (OPTIONAL) # Local development defaults to current directory # Docker deployments should use /app/data for persistence -# Channel IDs (from your dev server) -GUIDES_CHANNEL_ID=your-guide-channel-id -REPEL_LOG_CHANNEL_ID=your-repel-log-channel-id -ONBOARDING_CHANNEL_ID=onboarding-channel-id - -# Role IDs (from your dev server) -REPEL_ROLE_ID=your-repel-role-id -MODERATORS_ROLE_IDS=your-moderator-role-id -ONBOARDING_ROLE_ID=onboarding-role-id -# Other GUIDES_TRACKER_PATH=guides-tracker.json ADVENT_OF_CODE_TRACKER_PATH=advent-of-code-tracker.json