From 008834d3899122703bd6bc4da061cb71727928cd Mon Sep 17 00:00:00 2001 From: Fleny Date: Sat, 6 Jul 2024 19:37:33 +0200 Subject: [PATCH] docs: Reaction roles bot example (#3361) * Update docusaurus typescript setup for v3 And fix lint-staged and eslint * Enable automatic JSX runtime * Remove babel config and dependencies * update yarn.lock * add typecheck to site workflow * update typedoc config * downgrade docusaurus packages * Update site.yml * Type context and options in webpack-docusaurus-plugin.ts * Add reaction-roles code from docs example * Finish /roles reactions create command, missing event handler * Add handler for the role buttons * Initial update to reactionroles.md + code changes accordingly * Finish reactionroles.md file * Corrections to reactionroles.md * update deps & add --strip-leading-paths to swc * Add a note for the possibile ratelimit on command upsert * Update website/docs/examples/reactionroles.md Co-authored-by: LTS20050703 * Apply suggestions from code review Co-authored-by: LTS20050703 * Update website/docs/examples/reactionroles.md Co-authored-by: LTS20050703 * Use a register-commands.ts for app commands * Apply suggestions from code review Co-authored-by: LTS20050703 * Update for the latest version of discordeno Also add the tsconfig for the reaction roles example as i forgot it and was using the root dir one * Update deps, add typescript as dev deps, add .swcrc --------- Co-authored-by: Matt Hatcher <3768988+MatthewSH@users.noreply.github.com> Co-authored-by: Jonathan Ho Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Co-authored-by: LTS20050703 --- examples/reaction-roles/.env.example | 1 + examples/reaction-roles/.gitignore | 35 + examples/reaction-roles/.swcrc | 24 + examples/reaction-roles/README.md | 14 + examples/reaction-roles/package.json | 24 + examples/reaction-roles/src/collector.ts | 12 + examples/reaction-roles/src/commands/index.ts | 12 + examples/reaction-roles/src/commands/roles.ts | 476 ++++ examples/reaction-roles/src/events/index.ts | 10 + .../src/events/interactionCreate.ts | 58 + examples/reaction-roles/src/events/ready.ts | 7 + examples/reaction-roles/src/index.ts | 39 + .../reaction-roles/src/register-commands.ts | 8 + examples/reaction-roles/tsconfig.json | 14 + examples/reaction-roles/yarn.lock | 2101 +++++++++++++++++ website/docs/examples/reactionroles.md | 1391 +++++++++-- website/docusaurus.config.ts | 18 + website/src/styling/index.css | 40 + 18 files changed, 4117 insertions(+), 167 deletions(-) create mode 100644 examples/reaction-roles/.env.example create mode 100644 examples/reaction-roles/.gitignore create mode 100644 examples/reaction-roles/.swcrc create mode 100644 examples/reaction-roles/README.md create mode 100644 examples/reaction-roles/package.json create mode 100644 examples/reaction-roles/src/collector.ts create mode 100644 examples/reaction-roles/src/commands/index.ts create mode 100644 examples/reaction-roles/src/commands/roles.ts create mode 100644 examples/reaction-roles/src/events/index.ts create mode 100644 examples/reaction-roles/src/events/interactionCreate.ts create mode 100644 examples/reaction-roles/src/events/ready.ts create mode 100644 examples/reaction-roles/src/index.ts create mode 100644 examples/reaction-roles/src/register-commands.ts create mode 100644 examples/reaction-roles/tsconfig.json create mode 100644 examples/reaction-roles/yarn.lock diff --git a/examples/reaction-roles/.env.example b/examples/reaction-roles/.env.example new file mode 100644 index 000000000..3e3ec30d9 --- /dev/null +++ b/examples/reaction-roles/.env.example @@ -0,0 +1 @@ +TOKEN= # Your discord bot token diff --git a/examples/reaction-roles/.gitignore b/examples/reaction-roles/.gitignore new file mode 100644 index 000000000..6ad08f9b8 --- /dev/null +++ b/examples/reaction-roles/.gitignore @@ -0,0 +1,35 @@ +# dependencies +node_modules + +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +# testing +coverage + +# build +dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# turbo +.turbo diff --git a/examples/reaction-roles/.swcrc b/examples/reaction-roles/.swcrc new file mode 100644 index 000000000..751b8edf8 --- /dev/null +++ b/examples/reaction-roles/.swcrc @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "legacyDecorator": true, + "decoratorMetadata": true + }, + "target": "es2022", + "keepClassNames": true, + "loose": true + }, + "module": { + "type": "es6", + "strict": false, + "strictMode": true, + "lazy": false, + "noInterop": false + } +} diff --git a/examples/reaction-roles/README.md b/examples/reaction-roles/README.md new file mode 100644 index 000000000..58659a87e --- /dev/null +++ b/examples/reaction-roles/README.md @@ -0,0 +1,14 @@ +# Reaction roles example bot + +Example bot for reaction-roles using Discord Interactions and not classic reactions. + +## Setup + +1. Rename `.env.example` to `.env` and add your bot token in the `TOKEN` variable +1. Replace `REPLACE WITH YOUR GUILD ID` in `src/register-commands.ts` with your test guild id + +## Run the bot + +1. `yarn` to install the dependencies +1. `yarn build` to build the .ts files into .js +1. `yarn start` (or `node ./dist/index.js`) to run the bot diff --git a/examples/reaction-roles/package.json b/examples/reaction-roles/package.json new file mode 100644 index 000000000..416a9b07b --- /dev/null +++ b/examples/reaction-roles/package.json @@ -0,0 +1,24 @@ +{ + "name": "reaction-roles", + "description": "", + "main": "dist/index.js", + "type": "module", + "license": "ISC", + "private": true, + "packageManager": "yarn@4.0.2", + "scripts": { + "start": "node dist/index.js", + "build": "swc src --strip-leading-paths --delete-dir-on-start --out-dir dist", + "setup-dd": "" + }, + "dependencies": { + "@discordeno/bot": "19.0.0-next.ad7e74c", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@swc/cli": "^0.3.12", + "@swc/core": "^1.5.25", + "@types/node": "^20.14.2", + "typescript": "^5.4.5" + } +} diff --git a/examples/reaction-roles/src/collector.ts b/examples/reaction-roles/src/collector.ts new file mode 100644 index 000000000..42de1aee1 --- /dev/null +++ b/examples/reaction-roles/src/collector.ts @@ -0,0 +1,12 @@ +import { EventEmitter } from 'node:events' + +// Extremely minimal collector class +export default class ItemCollector extends EventEmitter { + onItem(callback: (item: T) => unknown): void { + this.on('item', callback) + } + + collect(item: T): void { + this.emit('item', item) + } +} diff --git a/examples/reaction-roles/src/commands/index.ts b/examples/reaction-roles/src/commands/index.ts new file mode 100644 index 000000000..e004b3c7f --- /dev/null +++ b/examples/reaction-roles/src/commands/index.ts @@ -0,0 +1,12 @@ +import type { Interaction } from '@discordeno/bot' +import type { CreateSlashApplicationCommand } from '@discordeno/types' +import roles from './roles.js' + +export const commands = new Map([roles].map((cmd) => [cmd.name, cmd])) + +export default commands + +export interface Command extends CreateSlashApplicationCommand { + /** Handler that will be executed when this command is triggered */ + execute: (interaction: Interaction, args: Record) => Promise +} diff --git a/examples/reaction-roles/src/commands/roles.ts b/examples/reaction-roles/src/commands/roles.ts new file mode 100644 index 000000000..b0cd7bbbc --- /dev/null +++ b/examples/reaction-roles/src/commands/roles.ts @@ -0,0 +1,476 @@ +import { + DiscordInteractionContextType, + MessageComponentTypes, + TextStyles, + type ActionRow, + type ButtonComponent, + type Interaction, + type Role, + type SelectMenuComponent, +} from '@discordeno/bot' +import { ApplicationCommandOptionTypes, ButtonStyles } from '@discordeno/types' +import ItemCollector from '../collector.js' +import { collectors } from '../events/interactionCreate.js' +import { bot } from '../index.js' +import type { Command } from './index.js' + +const command: Command = { + name: 'roles', + description: 'Role management on your server.', + // Do not allow to run this command in DM. Only in guilds. + contexts: [DiscordInteractionContextType.Guild], + // Require the user to have Manage Guild and Manage Roles by-default, server admins can override this setting for their server + defaultMemberPermissions: ['MANAGE_GUILD', 'MANAGE_ROLES'], + options: [ + { + name: 'reactions', + description: 'Manage the role reactions on your server.', + type: ApplicationCommandOptionTypes.SubCommandGroup, + options: [ + { + name: 'create', + description: 'Create a reaction role on your server.', + type: ApplicationCommandOptionTypes.SubCommand, + options: [ + { + name: 'role', + description: 'What role would you like to set for this button?', + type: ApplicationCommandOptionTypes.Role, + required: true, + }, + { + name: 'emoji', + description: "What would you like to set as this button's emoji?", + type: ApplicationCommandOptionTypes.String, + required: true, + }, + { + name: 'color', + description: "What color would you like to set as this button's color?", + type: ApplicationCommandOptionTypes.Integer, + required: true, + choices: [ + { name: 'Blue', value: ButtonStyles.Primary }, + { name: 'Green', value: ButtonStyles.Success }, + { name: 'Grey', value: ButtonStyles.Secondary }, + { name: 'Red', value: ButtonStyles.Danger }, + ], + }, + { + required: false, + name: 'label', + description: 'What would you like to set for the name on this button?', + type: ApplicationCommandOptionTypes.String, + // Discord imposed limit for button + maxLength: 80, + }, + ], + }, + ], + }, + ], + async execute(interaction, args: CommandArgs) { + // Create a reaction role + if (args.reactions?.create) { + // Ensure that there is a channelId + if (!interaction.channelId) { + await interaction.respond('Could not get the current channel.', { isPrivate: true }) + return + } + + // This array is used to store all the roles for this reaction roles + let roles = [args.reactions.create] + + // Send the message that uses will use to get the role + const roleMessage = await bot.helpers.sendMessage(interaction.channelId, { + content: 'Pick your roles', + components: getRoleButtons(roles), + }) + + // Create a copy of the actionRow for the main message + // NOTE: we use a copy so when we edit this actionRow the edits don't get applied to all the command executions, only this one, for example we do disable some buttons in some conditional cases + const messageActionRow = structuredClone(messageActionRowTemplate) + + await interaction.defer(true) + const message = await interaction.respond( + { + content: 'Use the buttons in this message to edit the message below.', + components: [messageActionRow], + }, + { isPrivate: true }, + ) + + if (!message) { + await interaction.respond('❌ Unable to send the message correctly. Cancelling', { isPrivate: true }) + return + } + + // Create the collector for the menu + const itemCollector = new ItemCollector() + collectors.add(itemCollector) + + // For the new reaction role, we need to keep track of what the user gave us + let partialRoleInfo: Partial<(typeof roles)[number]> | undefined + + itemCollector.onItem(async (i) => { + // We need to verify the interaction is for us. + if (i.message?.id !== message.id) { + return + } + + // Save button + if (i.data?.customId === 'reactionRoles-save') { + // Remove this item collector from the list of collectors (we aren't correcting anymore) + collectors.delete(itemCollector) + + // Delete the edit message + await i.deferEdit() + await i.delete() + + return + } + + // New button + if (i.data?.customId === 'reactionRoles-add') { + partialRoleInfo = {} + + // Ask the user for the role + await i.edit({ content: 'Pick a role for the new reaction role', components: [selectRoleActionRow] }) + return + } + + // New button - role select menu + if (partialRoleInfo && i.data?.customId === 'reactionRoles-add-role') { + const roleToAdd = i.data?.resolved?.roles?.first() + + // Verify that we could get the role from discord + if (!roleToAdd) { + throw new Error('Unable to get the information for the role to add') + } + + // Save it to our partial role information + partialRoleInfo.role = roleToAdd + + // Ask the user for the color of the button + await i.edit({ + content: 'Pick a color for the reaction role', + components: [selectColorActionRow], + }) + + return + } + + // New button - color select menu + if (partialRoleInfo && i.data?.customId === 'reactionRoles-add-color') { + const color = parseInt(i.data?.values?.[0] ?? 'NaN') + + // Verify that we could get the color information + if (isNaN(color)) { + throw new Error('Unable to get the information for the role to add') + } + + // Save the color to our partial + partialRoleInfo.color = color + + // Ask the user to input the emoji and optionally a label for the button + await i.respond({ + title: 'Pick an emoji and label for the reaction role', + components: [selectEmojiActionRow, selectLabelActionRow], + customId: 'reactionRoles-add-modal', + }) + + return + } + + // New button - emoji & label modal + if (partialRoleInfo && i.data?.customId === 'reactionRoles-add-modal') { + // Ensure that we can get the channelId from the interaction + if (!interaction.channelId) { + throw new Error('Unable to get current channel') + } + + // Get the data from discord + const emoji = i.data.components?.[0]?.components?.[0].value + const label = i.data.components?.[1]?.components?.[0].value + + // Verify that the emoji was given + if (!emoji) { + throw new Error('Unable to get the information for the role to add') + } + + // Save them to our partial + partialRoleInfo.emoji = emoji + partialRoleInfo.label = label + + // Save role and display the new message editing the old one + + // We are sure that in this place the entire object has been assembled + roles.push(partialRoleInfo as (typeof roles)[number]) + + await bot.helpers.editMessage(interaction.channelId, roleMessage.id, { + components: getRoleButtons(roles), + }) + + // Clear our partial roleInfo, we are done with it + partialRoleInfo = undefined + // In case the delete button was disabled (all the roles were deleted) re-enable it + messageActionRow.components[1]!.disabled = false + + // Discord imposes a limit of 5 action rows and 5 buttons for actionRow = 25 buttons max + // more than 25 will give an error, so we disable the new button + if (roles.length === 25) { + const button = messageActionRow.components[0] as ButtonComponent + button.disabled = true + } + + // Show again the main edit menu + await interaction.edit({ + content: 'Use the buttons in this message to edit the message below.', + components: [messageActionRow], + }) + + // Respond to the modal. A modal submit (type 5) interaction can't edit the original response + await i.respond('Reaction role created successfully. You can use the message above to add/remove a role', { isPrivate: true }) + + return + } + + // Remove button + if (i.data?.customId === 'reactionRoles-remove') { + // Clone the actionRow for the remove select menu, this is to prevent unwanted data to appear to other users + const removeActionRow = structuredClone(removeActionRowTemplate) + const selectMenu = removeActionRow.components[0] as SelectMenuComponent + + // Add the possible values for this select menu + for (const roleInfo of roles) { + selectMenu.options.push({ + label: `${roleInfo.emoji} ${roleInfo.label ?? ''}`, + value: roleInfo.role.id.toString(), + }) + } + + // Ask the user for what reaction role they want to remove + await i.edit({ + content: 'Select what reaction role to remove', + components: [removeActionRow], + }) + + return + } + + // Remove button - role select menu + if (i.data?.customId === 'reactionRoles-remove-selectMenu') { + // Ensure that we can get the channelId from the interaction + if (!interaction.channelId) { + throw new Error('Unable to get current channel') + } + + // Get the role to delete from discord + const roleToRemove = i.data?.values?.[0] + + // Ensure we got it + if (!roleToRemove) { + throw new Error('Unable to get the role to remove') + } + + await i.deferEdit() + + // Remove the role from the list + roles = roles.filter((roleInfo) => roleInfo.role.id.toString() !== roleToRemove) + + // Edit the main button + await bot.helpers.editMessage(interaction.channelId, roleMessage.id, { + components: getRoleButtons(roles), + }) + + // If the new button was disabled (we were at 25 buttons) we re-enable it + const button = messageActionRow.components[0] as ButtonComponent + button.disabled = false + + // If we are at 0 roles, and the user tried to delete a role they will get locked in the menu, so we disable it + if (roles.length === 0) { + messageActionRow.components[1]!.disabled = true + } + + // Show the main edit ui (new, remove, save) + await i.edit({ + content: 'Use the buttons in this message to edit the message below.', + components: [messageActionRow], + }) + + return + } + + // We don't know what code to run for this interaction + throw new Error('Unknown button') + }) + } + }, +} + +export default command + +// Interface to type the arguments that we receive from discord +interface CommandArgs { + reactions?: { + create?: { + role: Role + emoji: string + color: ButtonStyles + label?: string + } + } +} + +// Templates/ActionRows for the command to then be referenced in the various part of the code + +const messageActionRowTemplate: ActionRow = { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.Button, + style: ButtonStyles.Success, + customId: 'reactionRoles-add', + emoji: { + name: '➕', + }, + label: 'Add', + disabled: false, + }, + { + type: MessageComponentTypes.Button, + style: ButtonStyles.Danger, + customId: 'reactionRoles-remove', + emoji: { + name: '➖', + }, + label: 'Remove', + disabled: false, + }, + { + type: MessageComponentTypes.Button, + style: ButtonStyles.Success, + customId: 'reactionRoles-save', + emoji: { + name: '✅', + }, + label: 'Save', + }, + ], +} as const + +const removeActionRowTemplate: ActionRow = { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.SelectMenu, + customId: 'reactionRoles-remove-selectMenu', + maxValues: 1, + minValues: 1, + placeholder: 'Select roles', + options: [], + }, + ], +} as const + +const selectRoleActionRow: ActionRow = { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.SelectMenuRoles, + customId: 'reactionRoles-add-role', + maxValues: 1, + minValues: 1, + placeholder: 'Select a role', + }, + ], +} as const + +const selectColorActionRow: ActionRow = { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.SelectMenu, + customId: 'reactionRoles-add-color', + options: [ + { label: 'Blue', value: ButtonStyles.Primary.toString() }, + { label: 'Green', value: ButtonStyles.Success.toString() }, + { label: 'Grey', value: ButtonStyles.Secondary.toString() }, + { label: 'Red', value: ButtonStyles.Danger.toString() }, + ], + }, + ], +} as const + +const selectEmojiActionRow: ActionRow = { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.InputText, + style: TextStyles.Short, + customId: 'reactionRoles-add-emoji', + label: 'Emoji for the reaction role', + required: true, + }, + ], +} as const + +const selectLabelActionRow: ActionRow = { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.InputText, + style: TextStyles.Short, + customId: 'reactionRoles-add-label', + label: 'Label for the reaction role [OPTIONAL]', + // Discord imposed limit for button labels + maxLength: 80, + }, + ], +} as const + +// Function to get all the actionRows with buttons for the reaction roles message +function getRoleButtons( + roles: Array<{ + role: Role + emoji: string + color: ButtonStyles + label?: string | undefined + }>, +): ActionRow[] { + const actionRows: ActionRow[] = [] + + // If there aren't any roles, we don't need any buttons + if (roles.length === 0) return actionRows + + // We add the components later, so we need to make typescript know that we are sure that it will be a compatibile components array + actionRows.push({ type: MessageComponentTypes.ActionRow, components: [] as unknown as ActionRow['components'] }) + + for (const roleInfo of roles) { + let actionRow = actionRows.at(-1) + + // Ensure that we were able to get the actionRow + if (!actionRow) { + throw new Error('Unable to get actionRow') + } + + // If the actionRow is full (has 5 buttons) add a new one + if (actionRow.components.length === 5) { + actionRow = { type: MessageComponentTypes.ActionRow, components: [] as unknown as ActionRow['components'] } + actionRows.push(actionRow) + } + + // Add the new button to this actionRow + actionRow?.components.push({ + type: MessageComponentTypes.Button, + style: roleInfo.color, + emoji: { + name: roleInfo.emoji, + }, + label: roleInfo.label, + customId: `reactionRoles-role-${roleInfo.role.id}`, + }) + } + + return actionRows +} diff --git a/examples/reaction-roles/src/events/index.ts b/examples/reaction-roles/src/events/index.ts new file mode 100644 index 000000000..26fef6b73 --- /dev/null +++ b/examples/reaction-roles/src/events/index.ts @@ -0,0 +1,10 @@ +import type { EventHandlers } from '@discordeno/bot' +import { event as interactionCreateEvent } from './interactionCreate.js' +import { event as readyEvent } from './ready.js' + +export const events = { + interactionCreate: interactionCreateEvent, + ready: readyEvent, +} as Partial + +export default events diff --git a/examples/reaction-roles/src/events/interactionCreate.ts b/examples/reaction-roles/src/events/interactionCreate.ts new file mode 100644 index 000000000..a5662e2c2 --- /dev/null +++ b/examples/reaction-roles/src/events/interactionCreate.ts @@ -0,0 +1,58 @@ +import { InteractionTypes, MessageComponentTypes, commandOptionsParser, type EventHandlers, type Interaction } from '@discordeno/bot' +import type ItemCollector from '../collector.js' +import commands from '../commands/index.js' + +export const collectors = new Set>() + +export const event: EventHandlers['interactionCreate'] = async (interaction) => { + // Give to all the collectors the interaction to use + for (const collector of collectors) { + collector.collect(interaction) + } + + // If the interaction is a command check if it is a command and run it + if (interaction.type === InteractionTypes.ApplicationCommand) { + if (!interaction.data) return + + const command = commands.get(interaction.data.name) + if (!command) return + + try { + await command.execute(interaction, commandOptionsParser(interaction)) + } catch (error) { + console.error(error) + } + } + + // If the interaction is a button it might be the button press on our reaction role message + if (interaction.type === InteractionTypes.MessageComponent && interaction.data?.componentType === MessageComponentTypes.Button) { + // The interaction is not a button press on the role button + if (!interaction.data?.customId?.startsWith('reactionRoles-role-')) return + if (!interaction.guildId || !interaction.member) return + + // Remove the prefix and get the roleId + const roleId = BigInt(interaction.data.customId.slice('reactionRoles-role-'.length)) + + // Check if we need to remove or add the role to the user + const alreadyHasRole = !!interaction.member.roles.find((role) => role === roleId) + + try { + if (alreadyHasRole) { + await interaction.bot.helpers.removeRole(interaction.guildId, interaction.user.id, roleId, `Reaction role button for role id ${roleId}`) + await interaction.respond(`I removed from you the <@&${roleId}> role.`, { isPrivate: true }) + return + } + + // You will get an invalid request made if the bot attempts to give a bot role, a role higher then him hightest role, a link role or if it does not have the Manage Roles permission + // This could be prevented by checking for the roles that the bot owns and the role that the bot is trying to add + await interaction.bot.helpers.addRole(interaction.guildId, interaction.user.id, roleId, `Reaction role button for role id ${roleId}`) + await interaction.respond(`I added to you the <@&${roleId}> role.`, { isPrivate: true }) + } catch { + // Respond with an error message + await interaction.respond( + 'I could not give you the role. Possible reasons are:\n- My permissions are not configured correctly, make sure i have the `Manage Roles` permission\n- The role is **above** my hightest role in the server setup\n- The role does not exist or is non-manageable (for example: bot roles, link roles or @everyone)', + { isPrivate: true }, + ) + } + } +} diff --git a/examples/reaction-roles/src/events/ready.ts b/examples/reaction-roles/src/events/ready.ts new file mode 100644 index 000000000..17d0d3cf8 --- /dev/null +++ b/examples/reaction-roles/src/events/ready.ts @@ -0,0 +1,7 @@ +import type { EventHandlers } from '@discordeno/bot' +import { bot } from '../index.js' + +export const event: EventHandlers['ready'] = () => { + // Print to the console when the bot has connected to discord and is ready to handle the events + bot.logger.info('The bot is ready!') +} diff --git a/examples/reaction-roles/src/index.ts b/examples/reaction-roles/src/index.ts new file mode 100644 index 000000000..814c587cc --- /dev/null +++ b/examples/reaction-roles/src/index.ts @@ -0,0 +1,39 @@ +import { createBot } from '@discordeno/bot' +import { config } from 'dotenv' + +import events from './events/index.js' + +config() +const token = process.env.TOKEN + +// Ensure the existence of the TOKEN env +if (!token) throw new Error('The TOKEN environment variable needs to be defined.') + +export const bot = createBot({ + token, + events, +}) + +// Setup for the desiredProperties + +bot.transformers.desiredProperties.user.id = true + +bot.transformers.desiredProperties.message.id = true + +bot.transformers.desiredProperties.member.roles = true + +bot.transformers.desiredProperties.interaction.id = true +bot.transformers.desiredProperties.interaction.data = true +bot.transformers.desiredProperties.interaction.type = true +bot.transformers.desiredProperties.interaction.user = true +bot.transformers.desiredProperties.interaction.token = true +bot.transformers.desiredProperties.interaction.member = true +bot.transformers.desiredProperties.interaction.message = true +bot.transformers.desiredProperties.interaction.guildId = true +bot.transformers.desiredProperties.interaction.channelId = true + +bot.transformers.desiredProperties.role.id = true + +await bot.start() + +process.on('unhandledRejection', bot.logger.error) diff --git a/examples/reaction-roles/src/register-commands.ts b/examples/reaction-roles/src/register-commands.ts new file mode 100644 index 000000000..d8dc4817d --- /dev/null +++ b/examples/reaction-roles/src/register-commands.ts @@ -0,0 +1,8 @@ +import commands from './commands/index.js' +import { bot } from './index.js' + +const guildId = 'REPLACE WITH YOUR GUILD ID' + +await bot.rest + .upsertGuildApplicationCommands(guildId, [...commands.values()]) + .catch((e) => bot.logger.error('There was an error when updating the global commands', e)) diff --git a/examples/reaction-roles/tsconfig.json b/examples/reaction-roles/tsconfig.json new file mode 100644 index 000000000..04416ddd6 --- /dev/null +++ b/examples/reaction-roles/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "moduleResolution": "node", + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "strict": true, + "incremental": true + } +} diff --git a/examples/reaction-roles/yarn.lock b/examples/reaction-roles/yarn.lock new file mode 100644 index 000000000..a721d00c5 --- /dev/null +++ b/examples/reaction-roles/yarn.lock @@ -0,0 +1,2101 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10 + +"@discordeno/bot@npm:19.0.0-next.ad7e74c": + version: 19.0.0-next.ad7e74c + resolution: "@discordeno/bot@npm:19.0.0-next.ad7e74c" + dependencies: + "@discordeno/gateway": "npm:19.0.0-next.ad7e74c" + "@discordeno/rest": "npm:19.0.0-next.ad7e74c" + "@discordeno/types": "npm:19.0.0-next.ad7e74c" + "@discordeno/utils": "npm:19.0.0-next.ad7e74c" + checksum: 0bbcd98f1bb42bde47d262a67fa7b42ff4f5ef8f4556ef3117b8204cb864c6f26c0066320374ede6cf0e9730988a5c0b12472108dcf0cbc45b951db305a0671c + languageName: node + linkType: hard + +"@discordeno/gateway@npm:19.0.0-next.ad7e74c": + version: 19.0.0-next.ad7e74c + resolution: "@discordeno/gateway@npm:19.0.0-next.ad7e74c" + dependencies: + "@discordeno/types": "npm:19.0.0-next.ad7e74c" + "@discordeno/utils": "npm:19.0.0-next.ad7e74c" + ws: "npm:^8.16.0" + checksum: b16676060574a3a45c2388f7c73e62d7763bfdb87d5f0f84f673b7f68f34d5085e3a300c9a15f4120b2909ba776ef78336025c71541f68c50729379458ac9273 + languageName: node + linkType: hard + +"@discordeno/rest@npm:19.0.0-next.ad7e74c": + version: 19.0.0-next.ad7e74c + resolution: "@discordeno/rest@npm:19.0.0-next.ad7e74c" + dependencies: + "@discordeno/types": "npm:19.0.0-next.ad7e74c" + "@discordeno/utils": "npm:19.0.0-next.ad7e74c" + dotenv: "npm:^16.4.5" + checksum: d0fd00393b5845c17c862e9d8ce5197363d60db3073136b9b6f19b1fa5a20ea3020f180e627140f8e12a6cab65f736e3bbad1b376d959e49341efa81e43c65b8 + languageName: node + linkType: hard + +"@discordeno/types@npm:19.0.0-next.ad7e74c": + version: 19.0.0-next.ad7e74c + resolution: "@discordeno/types@npm:19.0.0-next.ad7e74c" + checksum: 3981908e1f88c22821a6abdc2056791a6dc75d09b9bc6ee96a1689871330745574c8824c314cf200b7df16d48215dc9b00e41f2fff03db26102c54422a82ab88 + languageName: node + linkType: hard + +"@discordeno/utils@npm:19.0.0-next.ad7e74c": + version: 19.0.0-next.ad7e74c + resolution: "@discordeno/utils@npm:19.0.0-next.ad7e74c" + dependencies: + "@discordeno/types": "npm:19.0.0-next.ad7e74c" + tweetnacl: "npm:^1.0.3" + checksum: 3b9265737208db205732e5f442d82c10a27952359a98ffb2f8835a4c94d43520d0490ab488faec68f40bd3205a42f4877804397eade83ada658a00e854270dc7 + languageName: node + linkType: hard + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: e9ed5fd27c3aec1095e3a16e0c0cf148d1fee55a38665c35f7b3f86a9b5d00d042ddaabc98e8a1cb7463b9378c15f22a94eb35e99469c201453eb8375191f243 + languageName: node + linkType: hard + +"@mole-inc/bin-wrapper@npm:^8.0.1": + version: 8.0.1 + resolution: "@mole-inc/bin-wrapper@npm:8.0.1" + dependencies: + bin-check: "npm:^4.1.0" + bin-version-check: "npm:^5.0.0" + content-disposition: "npm:^0.5.4" + ext-name: "npm:^5.0.0" + file-type: "npm:^17.1.6" + filenamify: "npm:^5.0.2" + got: "npm:^11.8.5" + os-filter-obj: "npm:^2.0.0" + checksum: 565df38f6f91fefe2e2540bf4357024fd912990aecb1873fd9bf21e6a667d9930c73e5cbc87a9aac0cf476b8dffc30a4e00ec97b62e713ef5c00d46823ea599d + languageName: node + linkType: hard + +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": "npm:2.0.5" + run-parallel: "npm:^1.1.9" + checksum: 6ab2a9b8a1d67b067922c36f259e3b3dfd6b97b219c540877a4944549a4d49ea5ceba5663905ab5289682f1f3c15ff441d02f0447f620a42e1cb5e1937174d4b + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.3": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": "npm:2.1.5" + fastq: "npm:^1.6.0" + checksum: 40033e33e96e97d77fba5a238e4bba4487b8284678906a9f616b5579ddaf868a18874c0054a75402c9fbaaa033a25ceae093af58c9c30278e35c23c9479e79b0 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^2.0.0": + version: 2.2.1 + resolution: "@npmcli/agent@npm:2.2.1" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.1" + checksum: d4a48128f61e47f2f5c89315a5350e265dc619987e635bd62b52b29c7ed93536e724e721418c0ce352ceece86c13043c67aba1b70c3f5cc72fce6bb746706162 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.0 + resolution: "@npmcli/fs@npm:3.1.0" + dependencies: + semver: "npm:^7.3.5" + checksum: f3a7ab3a31de65e42aeb6ed03ed035ef123d2de7af4deb9d4a003d27acc8618b57d9fb9d259fe6c28ca538032a028f37337264388ba27d26d37fff7dde22476e + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 115e8ceeec6bc69dff2048b35c0ab4f8bbee12d8bb6c1f4af758604586d802b6e669dcb02dda61d078de42c2b4ddce41b3d9e726d7daa6b4b850f4adbf7333ff + languageName: node + linkType: hard + +"@sindresorhus/is@npm:^4.0.0": + version: 4.6.0 + resolution: "@sindresorhus/is@npm:4.6.0" + checksum: e7f36ed72abfcd5e0355f7423a72918b9748bb1ef370a59f3e5ad8d40b728b85d63b272f65f63eec1faf417cda89dcb0aeebe94015647b6054659c1442fe5ce0 + languageName: node + linkType: hard + +"@swc/cli@npm:^0.3.12": + version: 0.3.12 + resolution: "@swc/cli@npm:0.3.12" + dependencies: + "@mole-inc/bin-wrapper": "npm:^8.0.1" + "@swc/counter": "npm:^0.1.3" + commander: "npm:^8.3.0" + fast-glob: "npm:^3.2.5" + minimatch: "npm:^9.0.3" + piscina: "npm:^4.3.0" + semver: "npm:^7.3.8" + slash: "npm:3.0.0" + source-map: "npm:^0.7.3" + peerDependencies: + "@swc/core": ^1.2.66 + chokidar: ^3.5.1 + peerDependenciesMeta: + chokidar: + optional: true + bin: + spack: bin/spack.js + swc: bin/swc.js + swcx: bin/swcx.js + checksum: fee260434fad8eed0328f4db17f42ce0864b93b90fcb02f3f0eb3aad994b5a6ae4bfdfb6d2329377e0cd8d07adc61cca42e290bf0005c1ab4d004d4a0b152db4 + languageName: node + linkType: hard + +"@swc/core-darwin-arm64@npm:1.5.25": + version: 1.5.25 + resolution: "@swc/core-darwin-arm64@npm:1.5.25" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-darwin-x64@npm:1.5.25": + version: 1.5.25 + resolution: "@swc/core-darwin-x64@npm:1.5.25" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@swc/core-linux-arm-gnueabihf@npm:1.5.25": + version: 1.5.25 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.5.25" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@swc/core-linux-arm64-gnu@npm:1.5.25": + version: 1.5.25 + resolution: "@swc/core-linux-arm64-gnu@npm:1.5.25" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-arm64-musl@npm:1.5.25": + version: 1.5.25 + resolution: "@swc/core-linux-arm64-musl@npm:1.5.25" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-linux-x64-gnu@npm:1.5.25": + version: 1.5.25 + resolution: "@swc/core-linux-x64-gnu@npm:1.5.25" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-x64-musl@npm:1.5.25": + version: 1.5.25 + resolution: "@swc/core-linux-x64-musl@npm:1.5.25" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-win32-arm64-msvc@npm:1.5.25": + version: 1.5.25 + resolution: "@swc/core-win32-arm64-msvc@npm:1.5.25" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-win32-ia32-msvc@npm:1.5.25": + version: 1.5.25 + resolution: "@swc/core-win32-ia32-msvc@npm:1.5.25" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@swc/core-win32-x64-msvc@npm:1.5.25": + version: 1.5.25 + resolution: "@swc/core-win32-x64-msvc@npm:1.5.25" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@swc/core@npm:^1.5.25": + version: 1.5.25 + resolution: "@swc/core@npm:1.5.25" + dependencies: + "@swc/core-darwin-arm64": "npm:1.5.25" + "@swc/core-darwin-x64": "npm:1.5.25" + "@swc/core-linux-arm-gnueabihf": "npm:1.5.25" + "@swc/core-linux-arm64-gnu": "npm:1.5.25" + "@swc/core-linux-arm64-musl": "npm:1.5.25" + "@swc/core-linux-x64-gnu": "npm:1.5.25" + "@swc/core-linux-x64-musl": "npm:1.5.25" + "@swc/core-win32-arm64-msvc": "npm:1.5.25" + "@swc/core-win32-ia32-msvc": "npm:1.5.25" + "@swc/core-win32-x64-msvc": "npm:1.5.25" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.7" + peerDependencies: + "@swc/helpers": "*" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 1ad878fe015d01c34ff20d8aee15b1cfb5cd66f9e8744e4be69e09628ade3c1108aa00c693da4eed6cc6ef08d686f6cab48a088ee61e933662eb8dd7b79d2e44 + languageName: node + linkType: hard + +"@swc/counter@npm:^0.1.3": + version: 0.1.3 + resolution: "@swc/counter@npm:0.1.3" + checksum: df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598 + languageName: node + linkType: hard + +"@swc/types@npm:^0.1.7": + version: 0.1.7 + resolution: "@swc/types@npm:0.1.7" + dependencies: + "@swc/counter": "npm:^0.1.3" + checksum: ed66c26b36972a74f852c1781fadc75946578abfeeea58f110684833b5d1e70f28a77ddb82fd5bf3cf3c4dad0e1b6a1c924d7e2cc7a99f9b16ed16fe266bba25 + languageName: node + linkType: hard + +"@szmarczak/http-timer@npm:^4.0.5": + version: 4.0.6 + resolution: "@szmarczak/http-timer@npm:4.0.6" + dependencies: + defer-to-connect: "npm:^2.0.0" + checksum: c29df3bcec6fc3bdec2b17981d89d9c9fc9bd7d0c9bcfe92821dc533f4440bc890ccde79971838b4ceed1921d456973c4180d7175ee1d0023ad0562240a58d95 + languageName: node + linkType: hard + +"@tokenizer/token@npm:^0.3.0": + version: 0.3.0 + resolution: "@tokenizer/token@npm:0.3.0" + checksum: 889c1f1e63ac7c92c0ea22d4a2861142f1b43c3d92eb70ec42aa9e9851fab2e9952211d50f541b287781280df2f979bf5600a9c1f91fbc61b7fcf9994e9376a5 + languageName: node + linkType: hard + +"@types/cacheable-request@npm:^6.0.1": + version: 6.0.3 + resolution: "@types/cacheable-request@npm:6.0.3" + dependencies: + "@types/http-cache-semantics": "npm:*" + "@types/keyv": "npm:^3.1.4" + "@types/node": "npm:*" + "@types/responselike": "npm:^1.0.0" + checksum: 159f9fdb2a1b7175eef453ae2ced5ea04c0d2b9610cc9ccd9f9abb066d36dacb1f37acd879ace10ad7cbb649490723feb396fb7307004c9670be29636304b988 + languageName: node + linkType: hard + +"@types/http-cache-semantics@npm:*": + version: 4.0.4 + resolution: "@types/http-cache-semantics@npm:4.0.4" + checksum: a59566cff646025a5de396d6b3f44a39ab6a74f2ed8150692e0f31cc52f3661a68b04afe3166ebe0d566bd3259cb18522f46e949576d5204781cd6452b7fe0c5 + languageName: node + linkType: hard + +"@types/keyv@npm:^3.1.4": + version: 3.1.4 + resolution: "@types/keyv@npm:3.1.4" + dependencies: + "@types/node": "npm:*" + checksum: e009a2bfb50e90ca9b7c6e8f648f8464067271fd99116f881073fa6fa76dc8d0133181dd65e6614d5fb1220d671d67b0124aef7d97dc02d7e342ab143a47779d + languageName: node + linkType: hard + +"@types/node@npm:*": + version: 20.11.0 + resolution: "@types/node@npm:20.11.0" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 8da60a8ccb65181c3d6f7686ddc5f1b1616cafa14d9e520a866adff82c17cc99336a78dd7ce7bee8f54e2332946f678b0e3aa377fbaaf751d3c05b64600872c6 + languageName: node + linkType: hard + +"@types/node@npm:^20.14.2": + version: 20.14.2 + resolution: "@types/node@npm:20.14.2" + dependencies: + undici-types: "npm:~5.26.4" + checksum: c38e47b190fa0a8bdfde24b036dddcf9401551f2fb170a90ff33625c7d6f218907e81c74e0fa6e394804a32623c24c60c50e249badc951007830f0d02c48ee0f + languageName: node + linkType: hard + +"@types/responselike@npm:^1.0.0": + version: 1.0.3 + resolution: "@types/responselike@npm:1.0.3" + dependencies: + "@types/node": "npm:*" + checksum: 6ac4b35723429b11b117e813c7acc42c3af8b5554caaf1fc750404c1ae59f9b7376bc69b9e9e194a5a97357a597c2228b7173d317320f0360d617b6425212f58 + languageName: node + linkType: hard + +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: ca0a54e35bea4ece0ecb68a47b312e1a9a6f772408d5bcb9051230aaa94b0460671c5b5c9cb3240eb5b7bc94c52476550eb221f65a0bbd0145bdc9f3113a6707 + languageName: node + linkType: hard + +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": + version: 7.1.0 + resolution: "agent-base@npm:7.1.0" + dependencies: + debug: "npm:^4.3.4" + checksum: f7828f991470a0cc22cb579c86a18cbae83d8a3cbed39992ab34fc7217c4d126017f1c74d0ab66be87f71455318a8ea3e757d6a37881b8d0f2a2c6aa55e5418f + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: "npm:^2.0.0" + indent-string: "npm:^4.0.0" + checksum: 1101a33f21baa27a2fa8e04b698271e64616b886795fd43c31068c07533c7b3facfcaf4e9e0cab3624bd88f729a592f1c901a1a229c9e490eafce411a8644b79 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: b4494dfbfc7e4591b4711a396bd27e540f8153914123dccb4cdbbcb514015ada63a3809f362b9d8d4f6b17a706f1d7bea3c6f974b15fa5ae76b5b502070889ff + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: 70fdf883b704d17a5dfc9cde206e698c16bcd74e7f196ab821511651aee4f9f76c9514bdfa6ca3a27b5e49138b89cb222a28caf3afe4567570139577f991df32 + languageName: node + linkType: hard + +"arch@npm:^2.1.0": + version: 2.2.0 + resolution: "arch@npm:2.2.0" + checksum: e35dbc6d362297000ab90930069576ba165fe63cd52383efcce14bd66c1b16a91ce849e1fd239964ed029d5e0bdfc32f68e9c7331b7df6c84ddebebfdbf242f7 + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 + languageName: node + linkType: hard + +"bin-check@npm:^4.1.0": + version: 4.1.0 + resolution: "bin-check@npm:4.1.0" + dependencies: + execa: "npm:^0.7.0" + executable: "npm:^4.1.0" + checksum: 16f6d5d86df9365dab682c7dd238f93678b773a908b3bccea4b1acb82b9b4e49fcfa24c99b99180a8e4cdd89a8f15f03700b09908ed5ae651f52fd82488a3507 + languageName: node + linkType: hard + +"bin-version-check@npm:^5.0.0": + version: 5.1.0 + resolution: "bin-version-check@npm:5.1.0" + dependencies: + bin-version: "npm:^6.0.0" + semver: "npm:^7.5.3" + semver-truncate: "npm:^3.0.0" + checksum: d99679cfe0964703045fe0145a98f117888942b621dfe2c2377305ee9a9d735374d8e3ecb3b476507b284af2567699f24f7ecb2feb1f27ad6086ad60b3198893 + languageName: node + linkType: hard + +"bin-version@npm:^6.0.0": + version: 6.0.0 + resolution: "bin-version@npm:6.0.0" + dependencies: + execa: "npm:^5.0.0" + find-versions: "npm:^5.0.0" + checksum: 78c29422ea9597eb4c8d4f0eff96df60d09aa82b53a87925bc403efbe5c55251b1a07baac538381d9096377f92d27e3c03963efa86db5bc0d6431b9563946229 + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 + languageName: node + linkType: hard + +"braces@npm:^3.0.2": + version: 3.0.2 + resolution: "braces@npm:3.0.2" + dependencies: + fill-range: "npm:^7.0.1" + checksum: 966b1fb48d193b9d155f810e5efd1790962f2c4e0829f8440b8ad236ba009222c501f70185ef732fef17a4c490bb33a03b90dab0631feafbdf447da91e8165b1 + languageName: node + linkType: hard + +"cacache@npm:^18.0.0": + version: 18.0.2 + resolution: "cacache@npm:18.0.2" + dependencies: + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^10.0.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: 5ca58464f785d4d64ac2019fcad95451c8c89bea25949f63acd8987fcc3493eaef1beccc0fa39e673506d879d3fc1ab420760f8a14f8ddf46ea2d121805a5e96 + languageName: node + linkType: hard + +"cacheable-lookup@npm:^5.0.3": + version: 5.0.4 + resolution: "cacheable-lookup@npm:5.0.4" + checksum: 618a8b3eea314060e74cb3285a6154e8343c244a34235acf91cfe626ee0705c24e3cd11e4b1a7b3900bd749ee203ae65afe13adf610c8ab173e99d4a208faf75 + languageName: node + linkType: hard + +"cacheable-request@npm:^7.0.2": + version: 7.0.4 + resolution: "cacheable-request@npm:7.0.4" + dependencies: + clone-response: "npm:^1.0.2" + get-stream: "npm:^5.1.0" + http-cache-semantics: "npm:^4.0.0" + keyv: "npm:^4.0.0" + lowercase-keys: "npm:^2.0.0" + normalize-url: "npm:^6.0.1" + responselike: "npm:^2.0.0" + checksum: 0f4f2001260ecca78b9f64fc8245e6b5a5dcde24ea53006daab71f5e0e1338095aa1512ec099c4f9895a9e5acfac9da423cb7c079e131485891e9214aca46c41 + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 2ac8cd2b2f5ec986a3c743935ec85b07bc174d5421a5efc8017e1f146a1cf5f781ae962618f416352103b32c9cd7e203276e8c28241bbe946160cab16149fb68 + languageName: node + linkType: hard + +"clone-response@npm:^1.0.2": + version: 1.0.3 + resolution: "clone-response@npm:1.0.3" + dependencies: + mimic-response: "npm:^1.0.0" + checksum: 4e671cac39b11c60aa8ba0a450657194a5d6504df51bca3fac5b3bd0145c4f8e8464898f87c8406b83232e3bc5cca555f51c1f9c8ac023969ebfbf7f6bdabb2e + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: fa00c91b4332b294de06b443923246bccebe9fab1b253f7fe1772d37b06a2269b4039a85e309abe1fe11b267b11c08d1d0473fda3badd6167f57313af2887a64 + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 + languageName: node + linkType: hard + +"commander@npm:^8.3.0": + version: 8.3.0 + resolution: "commander@npm:8.3.0" + checksum: 6b7b5d334483ce24bd73c5dac2eab901a7dbb25fd983ea24a1eeac6e7166bb1967f641546e8abf1920afbde86a45fbfe5812fbc69d0dc451bb45ca416a12a3a3 + languageName: node + linkType: hard + +"content-disposition@npm:^0.5.4": + version: 0.5.4 + resolution: "content-disposition@npm:0.5.4" + dependencies: + safe-buffer: "npm:5.2.1" + checksum: b7f4ce176e324f19324be69b05bf6f6e411160ac94bc523b782248129eb1ef3be006f6cff431aaea5e337fe5d176ce8830b8c2a1b721626ead8933f0cbe78720 + languageName: node + linkType: hard + +"cross-spawn@npm:^5.0.1": + version: 5.1.0 + resolution: "cross-spawn@npm:5.1.0" + dependencies: + lru-cache: "npm:^4.0.1" + shebang-command: "npm:^1.2.0" + which: "npm:^1.2.9" + checksum: 726939c9954fc70c20e538923feaaa33bebc253247d13021737c3c7f68cdc3e0a57f720c0fe75057c0387995349f3f12e20e9bfdbf12274db28019c7ea4ec166 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: e1a13869d2f57d974de0d9ef7acbf69dc6937db20b918525a01dacb5032129bd552d290d886d981e99f1b624cb03657084cc87bd40f115c07ecf376821c729ce + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.4": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 0073c3bcbd9cb7d71dd5f6b55be8701af42df3e56e911186dfa46fac3a5b9eb7ce7f377dd1d3be6db8977221f8eb333d945216f645cf56f6b688cd484837d255 + languageName: node + linkType: hard + +"decompress-response@npm:^6.0.0": + version: 6.0.0 + resolution: "decompress-response@npm:6.0.0" + dependencies: + mimic-response: "npm:^3.1.0" + checksum: d377cf47e02d805e283866c3f50d3d21578b779731e8c5072d6ce8c13cc31493db1c2f6784da9d1d5250822120cefa44f1deab112d5981015f2e17444b763812 + languageName: node + linkType: hard + +"defer-to-connect@npm:^2.0.0": + version: 2.0.1 + resolution: "defer-to-connect@npm:2.0.1" + checksum: 8a9b50d2f25446c0bfefb55a48e90afd58f85b21bcf78e9207cd7b804354f6409032a1705c2491686e202e64fc05f147aa5aa45f9aa82627563f045937f5791b + languageName: node + linkType: hard + +"dotenv@npm:^16.4.5": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 55a3134601115194ae0f924e54473459ed0d9fc340ae610b676e248cca45aa7c680d86365318ea964e6da4e2ea80c4514c1adab5adb43d6867fb57ff068f95c8 + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 9b1d3e1baefeaf7d70799db8774149cef33b97183a6addceeba0cf6b85ba23ee2686f302f14482006df32df75d32b17c509c143a3689627929e4a8efaf483952 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: c72d67a6821be15ec11997877c437491c313d924306b8da5d87d2a2bcc2cec9903cb5b04ee1a088460501d8e5b44f10df82fdc93c444101a7610b80c8b6938e1 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 915acf859cea7131dac1b2b5c9c8e35c4849e325a1d114c30adb8cd615970f6dca0e27f64f3a4949d7d6ed86ecd79a1c5c63f02e697513cddd7b5835c90948b8 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: bb98632f8ffa823996e508ce6a58ffcf5856330fde839ae42c9e1f436cc3b5cc651d4aeae72222916545428e54fd0f6aa8862fd8d25bdbcc4589f1e3f3715e7f + languageName: node + linkType: hard + +"end-of-stream@npm:^1.1.0": + version: 1.4.4 + resolution: "end-of-stream@npm:1.4.4" + dependencies: + once: "npm:^1.4.0" + checksum: 530a5a5a1e517e962854a31693dbb5c0b2fc40b46dad2a56a2deec656ca040631124f4795823acc68238147805f8b021abbe221f4afed5ef3c8e8efc2024908b + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 1d20d825cdcce8d811bfbe86340f4755c02655a7feb2f13f8c880566d9d72a3f6c92c192a6867632e490d6da67b678271f46e01044996a6443e870331100dfdd + languageName: node + linkType: hard + +"escape-string-regexp@npm:^5.0.0": + version: 5.0.0 + resolution: "escape-string-regexp@npm:5.0.0" + checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e + languageName: node + linkType: hard + +"execa@npm:^0.7.0": + version: 0.7.0 + resolution: "execa@npm:0.7.0" + dependencies: + cross-spawn: "npm:^5.0.1" + get-stream: "npm:^3.0.0" + is-stream: "npm:^1.1.0" + npm-run-path: "npm:^2.0.0" + p-finally: "npm:^1.0.0" + signal-exit: "npm:^3.0.0" + strip-eof: "npm:^1.0.0" + checksum: 7c1721de38e51d67cef2367b1f6037c4a89bc64993d93683f9f740fc74d6930dfc92faf2749b917b7337a8d632d7b86a4673400f762bc1fe4a977ea6b0f9055f + languageName: node + linkType: hard + +"execa@npm:^5.0.0": + version: 5.1.1 + resolution: "execa@npm:5.1.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^6.0.0" + human-signals: "npm:^2.1.0" + is-stream: "npm:^2.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^4.0.1" + onetime: "npm:^5.1.2" + signal-exit: "npm:^3.0.3" + strip-final-newline: "npm:^2.0.0" + checksum: 8ada91f2d70f7dff702c861c2c64f21dfdc1525628f3c0454fd6f02fce65f7b958616cbd2b99ca7fa4d474e461a3d363824e91b3eb881705231abbf387470597 + languageName: node + linkType: hard + +"executable@npm:^4.1.0": + version: 4.1.1 + resolution: "executable@npm:4.1.1" + dependencies: + pify: "npm:^2.2.0" + checksum: f01927ce59bccec804e171bf859a26e362c1f50aa9ebc69f7cafdcce3859d29d4b6267fd47237c18b0a1830614bd3f0ee14b7380d9bad18a4e7af9b5f0b6984f + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 2d9bbb6473de7051f96790d5f9a678f32e60ed0aa70741dc7fdc96fec8d631124ec3374ac144387604f05afff9500f31a1d45bd9eee4cdc2e4f9ad2d9b9d5dbd + languageName: node + linkType: hard + +"ext-list@npm:^2.0.0": + version: 2.2.2 + resolution: "ext-list@npm:2.2.2" + dependencies: + mime-db: "npm:^1.28.0" + checksum: fe69fedbef044e14d4ce9e84c6afceb696ba71500c15b8d0ce0a1e280237e17c95031b3d62d5e597652fea0065b9bf957346b3900d989dff59128222231ac859 + languageName: node + linkType: hard + +"ext-name@npm:^5.0.0": + version: 5.0.0 + resolution: "ext-name@npm:5.0.0" + dependencies: + ext-list: "npm:^2.0.0" + sort-keys-length: "npm:^1.0.0" + checksum: f598269bd5de4295540ea7d6f8f6a01d82a7508f148b7700a05628ef6121648d26e6e5e942049e953b3051863df6b54bd8fe951e7877f185e34ace5d44370b33 + languageName: node + linkType: hard + +"fast-glob@npm:^3.2.5": + version: 3.3.2 + resolution: "fast-glob@npm:3.3.2" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.4" + checksum: 222512e9315a0efca1276af9adb2127f02105d7288fa746145bf45e2716383fb79eb983c89601a72a399a56b7c18d38ce70457c5466218c5f13fad957cee16df + languageName: node + linkType: hard + +"fastq@npm:^1.6.0": + version: 1.16.0 + resolution: "fastq@npm:1.16.0" + dependencies: + reusify: "npm:^1.0.4" + checksum: de151543aab9d91900ed5da88860c46987ece925c628df586fac664235f25e020ec20729e1c032edb5fd2520fd4aa5b537d69e39b689e65e82112cfbecb4479e + languageName: node + linkType: hard + +"file-type@npm:^17.1.6": + version: 17.1.6 + resolution: "file-type@npm:17.1.6" + dependencies: + readable-web-to-node-stream: "npm:^3.0.2" + strtok3: "npm:^7.0.0-alpha.9" + token-types: "npm:^5.0.0-alpha.2" + checksum: 47c69b4046e31142492a135982b9a9e4873b368919a2e66d0ebdc04143b6d2e1225b4bec820668c442ef201b54d03569df08b6052edc6015b1022c236784e1c1 + languageName: node + linkType: hard + +"filename-reserved-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "filename-reserved-regex@npm:3.0.0" + checksum: 1803e19ce64d7cb88ee5a1bd3ce282470a5c263987269222426d889049fc857e302284fa71937de9582eba7a9f39539557d45e0562f2fa51cade8efc68c65dd9 + languageName: node + linkType: hard + +"filenamify@npm:^5.0.2": + version: 5.1.1 + resolution: "filenamify@npm:5.1.1" + dependencies: + filename-reserved-regex: "npm:^3.0.0" + strip-outer: "npm:^2.0.0" + trim-repeated: "npm:^2.0.0" + checksum: 55a7ed0858eb2655bb1bb1e945a59e3fb30ba4767f6924fa064ccd731bff07678aac3cb4f3899ae0e1621fe81d6472b5688232bb6afd4eeb989ade785fc1c6f1 + languageName: node + linkType: hard + +"fill-range@npm:^7.0.1": + version: 7.0.1 + resolution: "fill-range@npm:7.0.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: e260f7592fd196b4421504d3597cc76f4a1ca7a9488260d533b611fc3cefd61e9a9be1417cb82d3b01ad9f9c0ff2dbf258e1026d2445e26b0cf5148ff4250429 + languageName: node + linkType: hard + +"find-versions@npm:^5.0.0": + version: 5.1.0 + resolution: "find-versions@npm:5.1.0" + dependencies: + semver-regex: "npm:^4.0.5" + checksum: 680bdb0081f631f7bfb6f0f8edcfa0b74ab8cabc82097a4527a37b0d042aabc56685bf459ff27991eab0baddc04eb8e3bba8a2869f5004ecf7cdd2779b6e51de + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^4.0.1" + checksum: 087edd44857d258c4f73ad84cb8df980826569656f2550c341b27adf5335354393eec24ea2fabd43a253233fb27cee177ebe46bd0b7ea129c77e87cb1e9936fb + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 03191781e94bc9a54bd376d3146f90fe8e082627c502185dbf7b9b3032f66b0b142c1115f3b2cc5936575fc1b44845ce903dd4c21bec2a8d69f3bd56f9cee9ec + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: af143246cf6884fe26fa281621d45cfe111d34b30535a475bfa38dafe343dadb466c047a924ffc7d6b7b18265df4110224ce3803806dbb07173bf2087b648d7f + languageName: node + linkType: hard + +"get-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "get-stream@npm:3.0.0" + checksum: de14fbb3b4548ace9ab6376be852eef9898c491282e29595bc908a1814a126d3961b11cd4b7be5220019fe3b2abb84568da7793ad308fc139925a217063fa159 + languageName: node + linkType: hard + +"get-stream@npm:^5.1.0": + version: 5.2.0 + resolution: "get-stream@npm:5.2.0" + dependencies: + pump: "npm:^3.0.0" + checksum: 13a73148dca795e41421013da6e3ebff8ccb7fba4d2f023fd0c6da2c166ec4e789bec9774a73a7b49c08daf2cae552f8a3e914042ac23b5f59dd278cc8f9cbfb + languageName: node + linkType: hard + +"get-stream@npm:^6.0.0": + version: 6.0.1 + resolution: "get-stream@npm:6.0.1" + checksum: 781266d29725f35c59f1d214aedc92b0ae855800a980800e2923b3fbc4e56b3cb6e462c42e09a1cf1a00c64e056a78fa407cbe06c7c92b7e5cd49b4b85c2a497 + languageName: node + linkType: hard + +"glob-parent@npm:^5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: "npm:^4.0.1" + checksum: 32cd106ce8c0d83731966d31517adb766d02c3812de49c30cfe0675c7c0ae6630c11214c54a5ae67aca882cf738d27fd7768f21aa19118b9245950554be07247 + languageName: node + linkType: hard + +"glob@npm:^10.2.2, glob@npm:^10.3.10": + version: 10.3.10 + resolution: "glob@npm:10.3.10" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^2.3.5" + minimatch: "npm:^9.0.1" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry: "npm:^1.10.1" + bin: + glob: dist/esm/bin.mjs + checksum: 38bdb2c9ce75eb5ed168f309d4ed05b0798f640b637034800a6bf306f39d35409bf278b0eaaffaec07591085d3acb7184a201eae791468f0f617771c2486a6a8 + languageName: node + linkType: hard + +"got@npm:^11.8.5": + version: 11.8.6 + resolution: "got@npm:11.8.6" + dependencies: + "@sindresorhus/is": "npm:^4.0.0" + "@szmarczak/http-timer": "npm:^4.0.5" + "@types/cacheable-request": "npm:^6.0.1" + "@types/responselike": "npm:^1.0.0" + cacheable-lookup: "npm:^5.0.3" + cacheable-request: "npm:^7.0.2" + decompress-response: "npm:^6.0.0" + http2-wrapper: "npm:^1.0.0-beta.5.2" + lowercase-keys: "npm:^2.0.0" + p-cancelable: "npm:^2.0.0" + responselike: "npm:^2.0.0" + checksum: a30c74029d81bd5fe50dea1a0c970595d792c568e188ff8be254b5bc11e6158d1b014570772d4a30d0a97723e7dd34e7c8cc1a2f23018f60aece3070a7a5c2a5 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 362d5ed66b12ceb9c0a328fb31200b590ab1b02f4a254a697dc796850cc4385603e75f53ec59f768b2dad3bfa1464bd229f7de278d2899a0e3beffc634b6683f + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.0 + resolution: "http-proxy-agent@npm:7.0.0" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: dbaaf3d9f3fc4df4a5d7ec45d456ec50f575240b557160fa63427b447d1f812dd7fe4a4f17d2e1ba003d231f07edf5a856ea6d91cb32d533062ff20a7803ccac + languageName: node + linkType: hard + +"http2-wrapper@npm:^1.0.0-beta.5.2": + version: 1.0.3 + resolution: "http2-wrapper@npm:1.0.3" + dependencies: + quick-lru: "npm:^5.1.1" + resolve-alpn: "npm:^1.0.0" + checksum: 8097ee2699440c2e64bda52124990cc5b0fb347401c7797b1a0c1efd5a0f79a4ebaa68e8a6ac3e2dde5f09460c1602764da6da2412bad628ed0a3b0ae35e72d4 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.2 + resolution: "https-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: 9ec844f78fd643608239c9c3f6819918631df5cd3e17d104cc507226a39b5d4adda9d790fc9fd63ac0d2bb8a761b2f9f60faa80584a9bf9d7f2e8c5ed0acd330 + languageName: node + linkType: hard + +"human-signals@npm:^2.1.0": + version: 2.1.0 + resolution: "human-signals@npm:2.1.0" + checksum: df59be9e0af479036798a881d1f136c4a29e0b518d4abb863afbd11bf30efa3eeb1d0425fc65942dcc05ab3bf40205ea436b0ff389f2cd20b75b8643d539bf86 + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 24e3292dd3dadaa81d065c6f8c41b274a47098150d444b96e5f53b4638a9a71482921ea6a91a1f59bb71d9796de25e04afd05919fa64c360347ba65d3766f10f + languageName: node + linkType: hard + +"ieee754@npm:^1.2.1": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: d9f2557a59036f16c282aaeb107832dc957a93d73397d89bbad4eb1130560560eb695060145e8e6b3b498b15ab95510226649a0b8f52ae06583575419fe10fc4 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 2d30b157a91fe1c1d7c6f653cbf263f039be6c5bfa959245a16d4ee191fc0f2af86c08545b6e6beeb041c56b574d2d5b9f95343d378ab49c0f37394d541e7fc8 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: cd3f5cbc9ca2d624c6a1f53f12e6b341659aba0e2d3254ae2b4464aaea8b4294cdb09616abbc59458f980531f2429784ed6a420d48d245bcad0811980c9efae9 + languageName: node + linkType: hard + +"inherits@npm:^2.0.3": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 + languageName: node + linkType: hard + +"ip@npm:^2.0.0": + version: 2.0.0 + resolution: "ip@npm:2.0.0" + checksum: 1270b11e534a466fb4cf4426cbcc3a907c429389f7f4e4e3b288b42823562e88d6a509ceda8141a507de147ca506141f745005c0aa144569d94cf24a54eb52bc + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: df033653d06d0eb567461e58a7a8c9f940bd8c22274b94bf7671ab36df5719791aae15eef6d83bbb5e23283967f2f984b8914559d4449efda578c775c4be6f85 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 + languageName: node + linkType: hard + +"is-glob@npm:^4.0.1": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 3ed74f2b0cdf4f401f38edb0442ddfde3092d79d7d35c9919c86641efdbcbb32e45aa3c0f70ce5eecc946896cd5a0f26e4188b9f2b881876f7cb6c505b82da11 + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 93a32f01940220532e5948538699ad610d5924ac86093fcee83022252b363eb0cc99ba53ab084a04e4fb62bf7b5731f55496257a4c38adf87af9c4d352c71c35 + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 6a6c3383f68afa1e05b286af866017c78f1226d43ac8cb064e115ff9ed85eb33f5c4f7216c96a71e4dfea289ef52c5da3aef5bbfade8ffe47a0465d70c0c8e86 + languageName: node + linkType: hard + +"is-plain-obj@npm:^1.0.0": + version: 1.1.0 + resolution: "is-plain-obj@npm:1.1.0" + checksum: 0ee04807797aad50859652a7467481816cbb57e5cc97d813a7dcd8915da8195dc68c436010bf39d195226cde6a2d352f4b815f16f26b7bf486a5754290629931 + languageName: node + linkType: hard + +"is-stream@npm:^1.1.0": + version: 1.1.0 + resolution: "is-stream@npm:1.1.0" + checksum: 351aa77c543323c4e111204482808cfad68d2e940515949e31ccd0b010fc13d5fba4b9c230e4887fd24284713040f43e542332fbf172f6b9944b7d62e389c0ec + languageName: node + linkType: hard + +"is-stream@npm:^2.0.0": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 7c9f715c03aff08f35e98b1fadae1b9267b38f0615d501824f9743f3aab99ef10e303ce7db3f186763a0b70a19de5791ebfc854ff884d5a8c4d92211f642ec92 + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e + languageName: node + linkType: hard + +"jackspeak@npm:^2.3.5": + version: 2.3.6 + resolution: "jackspeak@npm:2.3.6" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 6e6490d676af8c94a7b5b29b8fd5629f21346911ebe2e32931c2a54210134408171c24cee1a109df2ec19894ad04a429402a8438cbf5cc2794585d35428ace76 + languageName: node + linkType: hard + +"json-buffer@npm:3.0.1": + version: 3.0.1 + resolution: "json-buffer@npm:3.0.1" + checksum: 82876154521b7b68ba71c4f969b91572d1beabadd87bd3a6b236f85fbc7dc4695089191ed60bb59f9340993c51b33d479f45b6ba9f3548beb519705281c32c3c + languageName: node + linkType: hard + +"keyv@npm:^4.0.0": + version: 4.5.4 + resolution: "keyv@npm:4.5.4" + dependencies: + json-buffer: "npm:3.0.1" + checksum: 167eb6ef64cc84b6fa0780ee50c9de456b422a1e18802209234f7c2cf7eae648c7741f32e50d7e24ccb22b24c13154070b01563d642755b156c357431a191e75 + languageName: node + linkType: hard + +"lowercase-keys@npm:^2.0.0": + version: 2.0.0 + resolution: "lowercase-keys@npm:2.0.0" + checksum: 1c233d2da35056e8c49fae8097ee061b8c799b2f02e33c2bf32f9913c7de8fb481ab04dab7df35e94156c800f5f34e99acbf32b21781d87c3aa43ef7b748b79e + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.2.0 + resolution: "lru-cache@npm:10.2.0" + checksum: 502ec42c3309c0eae1ce41afca471f831c278566d45a5273a0c51102dee31e0e250a62fa9029c3370988df33a14188a38e682c16143b794de78668de3643e302 + languageName: node + linkType: hard + +"lru-cache@npm:^4.0.1": + version: 4.1.5 + resolution: "lru-cache@npm:4.1.5" + dependencies: + pseudomap: "npm:^1.0.2" + yallist: "npm:^2.1.2" + checksum: 9ec7d73f11a32cba0e80b7a58fdf29970814c0c795acaee1a6451ddfd609bae6ef9df0837f5bbeabb571ecd49c1e2d79e10e9b4ed422cfba17a0cb6145b018a9 + languageName: node + linkType: hard + +"lru-cache@npm:^6.0.0": + version: 6.0.0 + resolution: "lru-cache@npm:6.0.0" + dependencies: + yallist: "npm:^4.0.0" + checksum: fc1fe2ee205f7c8855fa0f34c1ab0bcf14b6229e35579ec1fd1079f31d6fc8ef8eb6fd17f2f4d99788d7e339f50e047555551ebd5e434dda503696e7c6591825 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^13.0.0": + version: 13.0.0 + resolution: "make-fetch-happen@npm:13.0.0" + dependencies: + "@npmcli/agent": "npm:^2.0.0" + cacache: "npm:^18.0.0" + http-cache-semantics: "npm:^4.1.1" + is-lambda: "npm:^1.0.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^3.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + promise-retry: "npm:^2.0.1" + ssri: "npm:^10.0.0" + checksum: ded5a91a02b76381b06a4ec4d5c1d23ebbde15d402b3c3e4533b371dac7e2f7ca071ae71ae6dae72aa261182557b7b1b3fd3a705b39252dc17f74fa509d3e76f + languageName: node + linkType: hard + +"merge-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-stream@npm:2.0.0" + checksum: 6fa4dcc8d86629705cea944a4b88ef4cb0e07656ebf223fa287443256414283dd25d91c1cd84c77987f2aec5927af1a9db6085757cb43d90eb170ebf4b47f4f4 + languageName: node + linkType: hard + +"merge2@npm:^1.3.0": + version: 1.4.1 + resolution: "merge2@npm:1.4.1" + checksum: 7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 + languageName: node + linkType: hard + +"micromatch@npm:^4.0.4": + version: 4.0.5 + resolution: "micromatch@npm:4.0.5" + dependencies: + braces: "npm:^3.0.2" + picomatch: "npm:^2.3.1" + checksum: a749888789fc15cac0e03273844dbd749f9f8e8d64e70c564bcf06a033129554c789bb9e30d7566d7ff6596611a08e58ac12cf2a05f6e3c9c47c50c4c7e12fa2 + languageName: node + linkType: hard + +"mime-db@npm:^1.28.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 54bb60bf39e6f8689f6622784e668a3d7f8bed6b0d886f5c3c446cb3284be28b30bf707ed05d0fe44a036f8469976b2629bbea182684977b084de9da274694d7 + languageName: node + linkType: hard + +"mimic-fn@npm:^2.1.0": + version: 2.1.0 + resolution: "mimic-fn@npm:2.1.0" + checksum: d2421a3444848ce7f84bd49115ddacff29c15745db73f54041edc906c14b131a38d05298dae3081667627a59b2eb1ca4b436ff2e1b80f69679522410418b478a + languageName: node + linkType: hard + +"mimic-response@npm:^1.0.0": + version: 1.0.1 + resolution: "mimic-response@npm:1.0.1" + checksum: 034c78753b0e622bc03c983663b1cdf66d03861050e0c8606563d149bc2b02d63f62ce4d32be4ab50d0553ae0ffe647fc34d1f5281184c6e1e8cf4d85e8d9823 + languageName: node + linkType: hard + +"mimic-response@npm:^3.1.0": + version: 3.1.0 + resolution: "mimic-response@npm:3.1.0" + checksum: 7e719047612411fe071332a7498cf0448bbe43c485c0d780046c76633a771b223ff49bd00267be122cedebb897037fdb527df72335d0d0f74724604ca70b37ad + languageName: node + linkType: hard + +"minimatch@npm:^9.0.1, minimatch@npm:^9.0.3": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: c81b47d28153e77521877649f4bab48348d10938df9e8147a58111fe00ef89559a2938de9f6632910c4f7bf7bb5cd81191a546167e58d357f0cfb1e18cecc1c5 + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: b251bceea62090f67a6cced7a446a36f4cd61ee2d5cea9aee7fff79ba8030e416327a1c5aa2908dc22629d06214b46d88fdab8c51ac76bacbf5703851b5ad342 + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.4 + resolution: "minipass-fetch@npm:3.0.4" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: 3edf72b900e30598567eafe96c30374432a8709e61bb06b87198fa3192d466777e2ec21c52985a0999044fa6567bd6f04651585983a1cbb27e2c1770a07ed2a2 + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 40982d8d836a52b0f37049a0a7e5d0f089637298e6d9b45df9c115d4f0520682a78258905e5c8b180fb41b593b0a82cc1361d2c74b45f7ada66334f84d1ecfdd + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: a5c6ef069f70d9a524d3428af39f2b117ff8cd84172e19b754e7264a33df460873e6eb3d6e55758531580970de50ae950c496256bb4ad3691a2974cddff189f0 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 61682162d29f45d3152b78b08bab7fb32ca10899bc5991ffe98afc18c9e9543bd1e3be94f8b8373ba6262497db63607079dc242ea62e43e7b2270837b7347c93 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": + version: 7.0.4 + resolution: "minipass@npm:7.0.4" + checksum: e864bd02ceb5e0707696d58f7ce3a0b89233f0d686ef0d447a66db705c0846a8dc6f34865cd85256c1472ff623665f616b90b8ff58058b2ad996c5de747d2d18 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: ae0f45436fb51344dcb87938446a32fbebb540d0e191d63b35e1c773d47512e17307bf54aa88326cc6d176594d00e4423563a091f7266c2f9a6872cdc1e234d1 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: d71b8dcd4b5af2fe13ecf3bd24070263489404fe216488c5ba7e38ece1f54daf219e72a833a3a2dc404331e870e9f44963a33399589490956bff003a3404d3b2 + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: 673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f + languageName: node + linkType: hard + +"negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: 2723fb822a17ad55c93a588a4bc44d53b22855bf4be5499916ca0cab1e7165409d0b288ba2577d7b029f10ce18cf2ed8e703e5af31c984e1e2304277ef979837 + languageName: node + linkType: hard + +"nice-napi@npm:^1.0.2": + version: 1.0.2 + resolution: "nice-napi@npm:1.0.2" + dependencies: + node-addon-api: "npm:^3.0.0" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.2.2" + conditions: "!os=win32" + languageName: node + linkType: hard + +"node-addon-api@npm:^3.0.0": + version: 3.2.1 + resolution: "node-addon-api@npm:3.2.1" + dependencies: + node-gyp: "npm:latest" + checksum: 681b52dfa3e15b0a8e5cf283cc0d8cd5fd2a57c559ae670fcfd20544cbb32f75de7648674110defcd17ab2c76ebef630aa7d2d2f930bc7a8cc439b20fe233518 + languageName: node + linkType: hard + +"node-gyp-build@npm:^4.2.2": + version: 4.8.0 + resolution: "node-gyp-build@npm:4.8.0" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 80f410ab412df38e84171d3634a5716b6c6f14ecfa4eb971424d289381fb76f8bcbe1b666419ceb2c81060e558fd7c6d70cc0f60832bcca6a1559098925d9657 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 10.0.1 + resolution: "node-gyp@npm:10.0.1" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^13.0.0" + nopt: "npm:^7.0.0" + proc-log: "npm:^3.0.0" + semver: "npm:^7.3.5" + tar: "npm:^6.1.2" + which: "npm:^4.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 578cf0c821f258ce4b6ebce4461eca4c991a4df2dee163c0624f2fe09c7d6d37240be4942285a0048d307230248ee0b18382d6623b9a0136ce9533486deddfa8 + languageName: node + linkType: hard + +"nopt@npm:^7.0.0": + version: 7.2.0 + resolution: "nopt@npm:7.2.0" + dependencies: + abbrev: "npm:^2.0.0" + bin: + nopt: bin/nopt.js + checksum: 1e7489f17cbda452c8acaf596a8defb4ae477d2a9953b76eb96f4ec3f62c6b421cd5174eaa742f88279871fde9586d8a1d38fb3f53fa0c405585453be31dff4c + languageName: node + linkType: hard + +"normalize-url@npm:^6.0.1": + version: 6.1.0 + resolution: "normalize-url@npm:6.1.0" + checksum: 5ae699402c9d5ffa330adc348fcd6fc6e6a155ab7c811b96e30b7ecab60ceef821d8f86443869671dda71bbc47f4b9625739c82ad247e883e9aefe875bfb8659 + languageName: node + linkType: hard + +"npm-run-path@npm:^2.0.0": + version: 2.0.2 + resolution: "npm-run-path@npm:2.0.2" + dependencies: + path-key: "npm:^2.0.0" + checksum: acd5ad81648ba4588ba5a8effb1d98d2b339d31be16826a118d50f182a134ac523172101b82eab1d01cb4c2ba358e857d54cfafd8163a1ffe7bd52100b741125 + languageName: node + linkType: hard + +"npm-run-path@npm:^4.0.1": + version: 4.0.1 + resolution: "npm-run-path@npm:4.0.1" + dependencies: + path-key: "npm:^3.0.0" + checksum: 5374c0cea4b0bbfdfae62da7bbdf1e1558d338335f4cacf2515c282ff358ff27b2ecb91ffa5330a8b14390ac66a1e146e10700440c1ab868208430f56b5f4d23 + languageName: node + linkType: hard + +"once@npm:^1.3.1, once@npm:^1.4.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 + languageName: node + linkType: hard + +"onetime@npm:^5.1.2": + version: 5.1.2 + resolution: "onetime@npm:5.1.2" + dependencies: + mimic-fn: "npm:^2.1.0" + checksum: e9fd0695a01cf226652f0385bf16b7a24153dbbb2039f764c8ba6d2306a8506b0e4ce570de6ad99c7a6eb49520743afdb66edd95ee979c1a342554ed49a9aadd + languageName: node + linkType: hard + +"os-filter-obj@npm:^2.0.0": + version: 2.0.0 + resolution: "os-filter-obj@npm:2.0.0" + dependencies: + arch: "npm:^2.1.0" + checksum: 08808a109b2dba9be8686cc006e082a0f6595e6d87e2a30e4147cb1d22b62a30a6e5f4fd78226aee76d9158c84db3cea292adec02e6591452e93cb33bf5da877 + languageName: node + linkType: hard + +"p-cancelable@npm:^2.0.0": + version: 2.1.1 + resolution: "p-cancelable@npm:2.1.1" + checksum: 7f1b64db17fc54acf359167d62898115dcf2a64bf6b3b038e4faf36fc059e5ed762fb9624df8ed04b25bee8de3ab8d72dea9879a2a960cd12e23c420a4aca6ed + languageName: node + linkType: hard + +"p-finally@npm:^1.0.0": + version: 1.0.0 + resolution: "p-finally@npm:1.0.0" + checksum: 93a654c53dc805dd5b5891bab16eb0ea46db8f66c4bfd99336ae929323b1af2b70a8b0654f8f1eae924b2b73d037031366d645f1fd18b3d30cbd15950cc4b1d4 + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: 7ba4a2b1e24c05e1fc14bbaea0fc6d85cf005ae7e9c9425d4575550f37e2e584b1af97bcde78eacd7559208f20995988d52881334db16cf77bc1bcf68e48ed7c + languageName: node + linkType: hard + +"path-key@npm:^2.0.0": + version: 2.0.1 + resolution: "path-key@npm:2.0.1" + checksum: 6e654864e34386a2a8e6bf72cf664dcabb76574dd54013add770b374384d438aca95f4357bb26935b514a4e4c2c9b19e191f2200b282422a76ee038b9258c5e7 + languageName: node + linkType: hard + +"path-key@npm:^3.0.0, path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 + languageName: node + linkType: hard + +"path-scurry@npm:^1.10.1": + version: 1.10.1 + resolution: "path-scurry@npm:1.10.1" + dependencies: + lru-cache: "npm:^9.1.1 || ^10.0.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: eebfb8304fef1d4f7e1486df987e4fd77413de4fce16508dea69fcf8eb318c09a6b15a7a2f4c22877cec1cb7ecbd3071d18ca9de79eeece0df874a00f1f0bdc8 + languageName: node + linkType: hard + +"peek-readable@npm:^5.0.0": + version: 5.0.0 + resolution: "peek-readable@npm:5.0.0" + checksum: d342f02dd0c8a6b4bd0e7519a93d545b2b19375200e79a7431f0f1ec3f91e22b2217fa3a15cde95f6ab388ce6fce8aae75794d84b9b39c5836eb7c5f55e7ee9e + languageName: node + linkType: hard + +"picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 60c2595003b05e4535394d1da94850f5372c9427ca4413b71210f437f7b2ca091dbd611c45e8b37d10036fa8eade25c1b8951654f9d3973bfa66a2ff4d3b08bc + languageName: node + linkType: hard + +"pify@npm:^2.2.0": + version: 2.3.0 + resolution: "pify@npm:2.3.0" + checksum: 9503aaeaf4577acc58642ad1d25c45c6d90288596238fb68f82811c08104c800e5a7870398e9f015d82b44ecbcbef3dc3d4251a1cbb582f6e5959fe09884b2ba + languageName: node + linkType: hard + +"piscina@npm:^4.3.0": + version: 4.3.1 + resolution: "piscina@npm:4.3.1" + dependencies: + nice-napi: "npm:^1.0.2" + dependenciesMeta: + nice-napi: + optional: true + checksum: a56ec3507e3bae480a3d3feefc772911d72b1e996adeaf894d192b6075de81ff369c7a0f2137e20d23f399f1a5d910bf62682b21a0cfef1337f78b44ca04dfab + languageName: node + linkType: hard + +"proc-log@npm:^3.0.0": + version: 3.0.0 + resolution: "proc-log@npm:3.0.0" + checksum: 02b64e1b3919e63df06f836b98d3af002b5cd92655cab18b5746e37374bfb73e03b84fe305454614b34c25b485cc687a9eebdccf0242cda8fda2475dd2c97e02 + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 96e1a82453c6c96eef53a37a1d6134c9f2482f94068f98a59145d0986ca4e497bf110a410adf73857e588165eab3899f0ebcf7b3890c1b3ce802abc0d65967d4 + languageName: node + linkType: hard + +"pseudomap@npm:^1.0.2": + version: 1.0.2 + resolution: "pseudomap@npm:1.0.2" + checksum: 856c0aae0ff2ad60881168334448e898ad7a0e45fe7386d114b150084254c01e200c957cf378378025df4e052c7890c5bd933939b0e0d2ecfcc1dc2f0b2991f5 + languageName: node + linkType: hard + +"pump@npm:^3.0.0": + version: 3.0.0 + resolution: "pump@npm:3.0.0" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: e42e9229fba14732593a718b04cb5e1cfef8254544870997e0ecd9732b189a48e1256e4e5478148ecb47c8511dca2b09eae56b4d0aad8009e6fac8072923cfc9 + languageName: node + linkType: hard + +"queue-microtask@npm:^1.2.2": + version: 1.2.3 + resolution: "queue-microtask@npm:1.2.3" + checksum: 72900df0616e473e824202113c3df6abae59150dfb73ed13273503127235320e9c8ca4aaaaccfd58cf417c6ca92a6e68ee9a5c3182886ae949a768639b388a7b + languageName: node + linkType: hard + +"quick-lru@npm:^5.1.1": + version: 5.1.1 + resolution: "quick-lru@npm:5.1.1" + checksum: a516faa25574be7947969883e6068dbe4aa19e8ef8e8e0fd96cddd6d36485e9106d85c0041a27153286b0770b381328f4072aa40d3b18a19f5f7d2b78b94b5ed + languageName: node + linkType: hard + +"reaction-roles@workspace:.": + version: 0.0.0-use.local + resolution: "reaction-roles@workspace:." + dependencies: + "@discordeno/bot": "npm:19.0.0-next.ad7e74c" + "@swc/cli": "npm:^0.3.12" + "@swc/core": "npm:^1.5.25" + "@types/node": "npm:^20.14.2" + dotenv: "npm:^16.4.5" + typescript: "npm:^5.4.5" + languageName: unknown + linkType: soft + +"readable-stream@npm:^3.6.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048 + languageName: node + linkType: hard + +"readable-web-to-node-stream@npm:^3.0.2": + version: 3.0.2 + resolution: "readable-web-to-node-stream@npm:3.0.2" + dependencies: + readable-stream: "npm:^3.6.0" + checksum: d3a5bf9d707c01183d546a64864aa63df4d9cb835dfd2bf89ac8305e17389feef2170c4c14415a10d38f9b9bfddf829a57aaef7c53c8b40f11d499844bf8f1a4 + languageName: node + linkType: hard + +"resolve-alpn@npm:^1.0.0": + version: 1.2.1 + resolution: "resolve-alpn@npm:1.2.1" + checksum: 744e87888f0b6fa0b256ab454ca0b9c0b80808715e2ef1f3672773665c92a941f6181194e30ccae4a8cd0adbe0d955d3f133102636d2ee0cca0119fec0bc9aec + languageName: node + linkType: hard + +"responselike@npm:^2.0.0": + version: 2.0.1 + resolution: "responselike@npm:2.0.1" + dependencies: + lowercase-keys: "npm:^2.0.0" + checksum: b122535466e9c97b55e69c7f18e2be0ce3823c5d47ee8de0d9c0b114aa55741c6db8bfbfce3766a94d1272e61bfb1ebf0a15e9310ac5629fbb7446a861b4fd3a + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 1f914879f97e7ee931ad05fe3afa629bd55270fc6cf1c1e589b6a99fab96d15daad0fa1a52a00c729ec0078045fe3e399bd4fd0c93bcc906957bdc17f89cb8e6 + languageName: node + linkType: hard + +"reusify@npm:^1.0.4": + version: 1.0.4 + resolution: "reusify@npm:1.0.4" + checksum: 14222c9e1d3f9ae01480c50d96057228a8524706db79cdeb5a2ce5bb7070dd9f409a6f84a02cbef8cdc80d39aef86f2dd03d155188a1300c599b05437dcd2ffb + languageName: node + linkType: hard + +"run-parallel@npm:^1.1.9": + version: 1.2.0 + resolution: "run-parallel@npm:1.2.0" + dependencies: + queue-microtask: "npm:^1.2.2" + checksum: cb4f97ad25a75ebc11a8ef4e33bb962f8af8516bb2001082ceabd8902e15b98f4b84b4f8a9b222e5d57fc3bd1379c483886ed4619367a7680dad65316993021d + languageName: node + linkType: hard + +"safe-buffer@npm:5.2.1, safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 32872cd0ff68a3ddade7a7617b8f4c2ae8764d8b7d884c651b74457967a9e0e886267d3ecc781220629c44a865167b61c375d2da6c720c840ecd73f45d5d9451 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 7eaf7a0cf37cc27b42fb3ef6a9b1df6e93a1c6d98c6c6702b02fe262d5fcbd89db63320793b99b21cb5348097d0a53de81bd5f4e8b86e20cc9412e3f1cfb4e83 + languageName: node + linkType: hard + +"semver-regex@npm:^4.0.5": + version: 4.0.5 + resolution: "semver-regex@npm:4.0.5" + checksum: b9e5c0573c4a997fb7e6e76321385d254797e86c8dba5e23f3cd8cf8f40b40414097a51514e5fead61dcb88ff10d3676355c01e2040f3c68f6c24bfd2073da2e + languageName: node + linkType: hard + +"semver-truncate@npm:^3.0.0": + version: 3.0.0 + resolution: "semver-truncate@npm:3.0.0" + dependencies: + semver: "npm:^7.3.5" + checksum: d8c23812218ff147f512ac4830e86860a377dba8a9733ae97d816102aca33236fa1c44c06544727153fffb93d15d0e45c49b2c40a7964aa3671769e9aed2f3f9 + languageName: node + linkType: hard + +"semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3": + version: 7.5.4 + resolution: "semver@npm:7.5.4" + dependencies: + lru-cache: "npm:^6.0.0" + bin: + semver: bin/semver.js + checksum: 985dec0d372370229a262c737063860fabd4a1c730662c1ea3200a2f649117761a42184c96df62a0e885e76fbd5dace41087d6c1ac0351b13c0df5d6bcb1b5ac + languageName: node + linkType: hard + +"shebang-command@npm:^1.2.0": + version: 1.2.0 + resolution: "shebang-command@npm:1.2.0" + dependencies: + shebang-regex: "npm:^1.0.0" + checksum: 9eed1750301e622961ba5d588af2212505e96770ec376a37ab678f965795e995ade7ed44910f5d3d3cb5e10165a1847f52d3348c64e146b8be922f7707958908 + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa + languageName: node + linkType: hard + +"shebang-regex@npm:^1.0.0": + version: 1.0.0 + resolution: "shebang-regex@npm:1.0.0" + checksum: 404c5a752cd40f94591dfd9346da40a735a05139dac890ffc229afba610854d8799aaa52f87f7e0c94c5007f2c6af55bdcaeb584b56691926c5eaf41dc8f1372 + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222 + languageName: node + linkType: hard + +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.3": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f + languageName: node + linkType: hard + +"slash@npm:3.0.0": + version: 3.0.0 + resolution: "slash@npm:3.0.0" + checksum: 94a93fff615f25a999ad4b83c9d5e257a7280c90a32a7cb8b4a87996e4babf322e469c42b7f649fd5796edd8687652f3fb452a86dc97a816f01113183393f11c + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 927484aa0b1640fd9473cee3e0a0bcad6fce93fd7bbc18bac9ad0c33686f5d2e2c422fba24b5899c184524af01e11dd2bd051c2bf2b07e47aff8ca72cbfc60d2 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.1": + version: 8.0.2 + resolution: "socks-proxy-agent@npm:8.0.2" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:^4.3.4" + socks: "npm:^2.7.1" + checksum: ea727734bd5b2567597aa0eda14149b3b9674bb44df5937bbb9815280c1586994de734d965e61f1dd45661183d7b41f115fb9e432d631287c9063864cfcc2ecc + languageName: node + linkType: hard + +"socks@npm:^2.7.1": + version: 2.7.1 + resolution: "socks@npm:2.7.1" + dependencies: + ip: "npm:^2.0.0" + smart-buffer: "npm:^4.2.0" + checksum: 5074f7d6a13b3155fa655191df1c7e7a48ce3234b8ccf99afa2ccb56591c195e75e8bb78486f8e9ea8168e95a29573cbaad55b2b5e195160ae4d2ea6811ba833 + languageName: node + linkType: hard + +"sort-keys-length@npm:^1.0.0": + version: 1.0.1 + resolution: "sort-keys-length@npm:1.0.1" + dependencies: + sort-keys: "npm:^1.0.0" + checksum: f9acac5fb31580a9e3d43b419dc86a1b75e85b79036a084d95dd4d1062b621c9589906588ac31e370a0dd381be46d8dbe900efa306d087ca9c912d7a59b5a590 + languageName: node + linkType: hard + +"sort-keys@npm:^1.0.0": + version: 1.1.2 + resolution: "sort-keys@npm:1.1.2" + dependencies: + is-plain-obj: "npm:^1.0.0" + checksum: 0ac2ea2327d92252f07aa7b2f8c7023a1f6ce3306439a3e81638cce9905893c069521d168f530fb316d1a929bdb052b742969a378190afaef1bc64fa69e29576 + languageName: node + linkType: hard + +"source-map@npm:^0.7.3": + version: 0.7.4 + resolution: "source-map@npm:0.7.4" + checksum: a0f7c9b797eda93139842fd28648e868a9a03ea0ad0d9fa6602a0c1f17b7fb6a7dcca00c144476cccaeaae5042e99a285723b1a201e844ad67221bf5d428f1dc + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.5 + resolution: "ssri@npm:10.0.5" + dependencies: + minipass: "npm:^7.0.3" + checksum: 453f9a1c241c13f5dfceca2ab7b4687bcff354c3ccbc932f35452687b9ef0ccf8983fd13b8a3baa5844c1a4882d6e3ddff48b0e7fd21d743809ef33b80616d79 + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 + languageName: node + linkType: hard + +"string_decoder@npm:^1.1.1": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 54d23f4a6acae0e93f999a585e673be9e561b65cd4cca37714af1e893ab8cd8dfa52a9e4f58f48f87b4a44918d3a9254326cb80ed194bf2e4c226e2b21767e56 + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: ae3b5436d34fadeb6096367626ce987057713c566e1e7768818797e00ac5d62023d0f198c4e681eae9e20701721980b26a64a8f5b91238869592a9c6800719a2 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 475f53e9c44375d6e72807284024ac5d668ee1d06010740dec0b9744f2ddf47de8d7151f80e5f6190fc8f384e802fdf9504b76a7e9020c9faee7103623338be2 + languageName: node + linkType: hard + +"strip-eof@npm:^1.0.0": + version: 1.0.0 + resolution: "strip-eof@npm:1.0.0" + checksum: 40bc8ddd7e072f8ba0c2d6d05267b4e0a4800898c3435b5fb5f5a21e6e47dfaff18467e7aa0d1844bb5d6274c3097246595841fbfeb317e541974ee992cac506 + languageName: node + linkType: hard + +"strip-final-newline@npm:^2.0.0": + version: 2.0.0 + resolution: "strip-final-newline@npm:2.0.0" + checksum: 69412b5e25731e1938184b5d489c32e340605bb611d6140344abc3421b7f3c6f9984b21dff296dfcf056681b82caa3bb4cc996a965ce37bcfad663e92eae9c64 + languageName: node + linkType: hard + +"strip-outer@npm:^2.0.0": + version: 2.0.0 + resolution: "strip-outer@npm:2.0.0" + checksum: 14ef9fe861e59a5f1555f1860982ae4edce2edb4ed34ab1b37cb62a8ba2f7c3540cbca6c884eabe4006e6cd729ab5d708a631169dd5b66fda570836e7e3b6589 + languageName: node + linkType: hard + +"strtok3@npm:^7.0.0-alpha.9": + version: 7.0.0 + resolution: "strtok3@npm:7.0.0" + dependencies: + "@tokenizer/token": "npm:^0.3.0" + peek-readable: "npm:^5.0.0" + checksum: 4f2269679fcfce1e9fe0600eff361ea4c687ae0a0e8d9dab6703811071cd92545cbcb32d4ace3d3aa591f777cec1a3e8aeecd5efd71ae216fd2962a7a238b4ab + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.1.2": + version: 6.2.0 + resolution: "tar@npm:6.2.0" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: 2042bbb14830b5cd0d584007db0eb0a7e933e66d1397e72a4293768d2332449bc3e312c266a0887ec20156dea388d8965e53b4fc5097f42d78593549016da089 + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: "npm:^7.0.0" + checksum: 10dda13571e1f5ad37546827e9b6d4252d2e0bc176c24a101252153ef435d83696e2557fe128c4678e4e78f5f01e83711c703eef9814eb12dab028580d45980a + languageName: node + linkType: hard + +"token-types@npm:^5.0.0-alpha.2": + version: 5.0.1 + resolution: "token-types@npm:5.0.1" + dependencies: + "@tokenizer/token": "npm:^0.3.0" + ieee754: "npm:^1.2.1" + checksum: 0985369bbea9f53a5ccd79bb9899717b41401a813deb2c7fb1add5d0baf2f702aaf6da78f6e0ccf346d5a9f7acaa7cb5efed7d092d89d8c1e6962959e9509bc0 + languageName: node + linkType: hard + +"trim-repeated@npm:^2.0.0": + version: 2.0.0 + resolution: "trim-repeated@npm:2.0.0" + dependencies: + escape-string-regexp: "npm:^5.0.0" + checksum: 4086eb0bc560f3da0370f427f423db4e3fc0a8e1560ecffc3b68512071319fe82dc9dd86d76b981d36ada76d7d49c3f8897ac054c87bc177e7a25abfd29e2bcd + languageName: node + linkType: hard + +"tweetnacl@npm:^1.0.3": + version: 1.0.3 + resolution: "tweetnacl@npm:1.0.3" + checksum: ca122c2f86631f3c0f6d28efb44af2a301d4a557a62a3e2460286b08e97567b258c2212e4ad1cfa22bd6a57edcdc54ba76ebe946847450ab0999e6d48ccae332 + languageName: node + linkType: hard + +"typescript@npm:^5.4.5": + version: 5.4.5 + resolution: "typescript@npm:5.4.5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: d04a9e27e6d83861f2126665aa8d84847e8ebabcea9125b9ebc30370b98cb38b5dff2508d74e2326a744938191a83a69aa9fddab41f193ffa43eabfdf3f190a5 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.4.5#optional!builtin": + version: 5.4.5 + resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin::version=5.4.5&hash=e012d7" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 584be8bac7112ad49a9eb9992f71d542b1ff2fafb5bb315e1c196145e8feab589f1d7223cfb2d5df6770789582e6918f8287d1f2f89911b38eb80e29c560ad00 + languageName: node + linkType: hard + +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: "npm:^4.0.0" + checksum: 8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 40912a8963fc02fb8b600cf50197df4a275c602c60de4cac4f75879d3c48558cfac48de08a25cc10df8112161f7180b3bbb4d662aadb711568602f9eddee54f0 + languageName: node + linkType: hard + +"util-deprecate@npm:^1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 + languageName: node + linkType: hard + +"which@npm:^1.2.9": + version: 1.3.1 + resolution: "which@npm:1.3.1" + dependencies: + isexe: "npm:^2.0.0" + bin: + which: ./bin/which + checksum: 549dcf1752f3ee7fbb64f5af2eead4b9a2f482108b7de3e85c781d6c26d8cf6a52d37cfbe0642a155fa6470483fe892661a859c03157f24c669cf115f3bbab5e + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 4782f8a1d6b8fc12c65e968fea49f59752bf6302dc43036c3bf87da718a80710f61a062516e9764c70008b487929a73546125570acea95c5b5dcc8ac3052c70f + languageName: node + linkType: hard + +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" + dependencies: + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: f17e84c042592c21e23c8195108cff18c64050b9efb8459589116999ea9da6dd1509e6a1bac3aeebefd137be00fabbb61b5c2bc0aa0f8526f32b58ee2f545651 + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: cebdaeca3a6880da410f75209e68cd05428580de5ad24535f22696d7d9cab134d1f8498599f344c3cf0fb37c1715807a183778d8c648d6cc0cb5ff2bb4236540 + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 7b1e4b35e9bb2312d2ee9ee7dc95b8cb5f8b4b5a89f7dde5543fe66c1e3715663094defa50d75454ac900bd210f702d575f15f3f17fa9ec0291806d2578d1ddf + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 + languageName: node + linkType: hard + +"ws@npm:^8.16.0": + version: 8.17.0 + resolution: "ws@npm:8.17.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 5e1dcb0ae70c6e2f158f5b446e0a72a2cd335b07aba73ee1872e9bae1285382286a10e53ed479db21bdd690a5dfd05641a768611ebb236253c62fefa43ef58b4 + languageName: node + linkType: hard + +"yallist@npm:^2.1.2": + version: 2.1.2 + resolution: "yallist@npm:2.1.2" + checksum: 75fc7bee4821f52d1c6e6021b91b3e079276f1a9ce0ad58da3c76b79a7e47d6f276d35e206a96ac16c1cf48daee38a8bb3af0b1522a3d11c8ffe18f898828832 + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 4cb02b42b8a93b5cf50caf5d8e9beb409400a8a4d85e83bb0685c1457e9ac0b7a00819e9f5991ac25ffabb56a78e2f017c1acc010b3a1babfe6de690ba531abd + languageName: node + linkType: hard diff --git a/website/docs/examples/reactionroles.md b/website/docs/examples/reactionroles.md index 8c9ab8dc2..4c9c88e14 100644 --- a/website/docs/examples/reactionroles.md +++ b/website/docs/examples/reactionroles.md @@ -1,28 +1,28 @@ --- sidebar_position: 1 -sidebar_label: Reaction Roles(button) Bot +sidebar_label: Reaction Roles (button) Bot --- # Reaction Roles Bot -One of the most popular bot features is reaction roles. We are going to look into making a small reaction roles bot as it will give us a chance to learn Discordeno while also making a nice little feature. However, instead of **reactions** we will be using Discord's new interaction API and use **buttons** instead. Using buttons will give us a lot of advantages. For example, this can be done without needing cache or database at all. This means it is possible to add this feature to your bot with minimal cost even at scale, since you do not store anything to make it work. +In this guide we will make a simple reaction roles bot. This will give us an opportunity to learn Discordeno while also making one of the most popular bot features. However, instead of **reactions** we will be using Discord's interaction API and use **buttons** instead. Using buttons will give us a lot of advantages. For example, this can be done without needing cache or database at all. This means it is possible to add this feature to your bot with minimal cost even at scale, since you do not need to store anything to make it work. ## Pre-Requirements -Before, going forward, please make sure to have finished everything on this list. +Before going forward, please make sure you have finished everything on this list. -- [ ] Create an application and get the bot token. [Create Application Guide](https://discordeno.js.org/docs/beginner/token) -- [ ] Add your bot to a server you own. [Invite Bot Guide](https://discordeno.js.org/docs/beginner/inviting) -- [ ] Create a GitHub repo for this project. [Setup Repo Guide](https://discordeno.js.org/docs/beginner/github) -- [ ] Install Discordeno. [Installation Guide](https://discordeno.js.org) -- [ ] Setup environment variables. [Environment Variables Guide](https://discordeno.js.org/docs/beginner/env) +- Create an application and get the bot token. [Create Application Guide](../beginner/token.md) +- Add your bot to a server you own. [Invite Bot Guide](../beginner/inviting.md) +- [recommended, optional] Create a GitHub repo for this project. [Setup Repo Guide](../beginner/github.md) +- Install Discordeno. [Installation Guide](../getting-started.md) +- Setup environment variables. [Environment Variables Guide](../beginner/env.md) ## Creating Our Bot -First let's go ahead and set up the base files we need to make this work. Create an `index.ts` file. +First let's go ahead and set up the base files we need to make this work. Create an `src/index.ts` file. ```ts -import { createBot, logger } from '@discordeno/bot' +import { createBot } from '@discordeno/bot' import { config } from 'dotenv' config() @@ -31,9 +31,7 @@ const bot = createBot({ events: {}, }) -logger.info(`[Startup] Starting bot.`) await bot.start() -logger.info(`[Startup] Bot started successfully.`) ``` ## Creating A Reaction Role @@ -46,23 +44,33 @@ export const command = {} export default command ``` -Now we can add the command type to this object, to give us the ability to have typescript help us autocomplete some stuff. +Now we can add the command type to this object, to give TypeScript the ability to help us autocompleting some stuff. ```ts +// insert-next-line import { CreateApplicationCommand } from '@discordeno/types' +// remove-next-line +export const command = {} +// insert-next-line export const command: CreateApplicationCommand = {} ``` -By now, you should be seeing some TypeScript errors so let's fix that. +By now, you should be seeing some TypeScript errors so let's fix those. + +Typescript requires us to add: + +- `name` - The name property is used as the main command name +- `description` - A description for what this command does ```ts import { CreateApplicationCommand } from '@discordeno/types' export const command: CreateApplicationCommand = { + // insert-start name: 'roles', description: 'Role management on your server.', - options: [], + // insert-end } ``` @@ -70,133 +78,168 @@ export const command: CreateApplicationCommand = { Nice, so we now have our basic command, `/roles` ready. Next, we should prepare our `/roles reactions` subcommand here. +To do this we need to tell Discord that this `/roles` command has some options, which can be done with an `options` array. Inside the `options` array we can add the `reactions` subcommand, which will require us to add a name, a description and a type. To make Discord create a group of subcommands named `reactions`, we will use `ApplicationCommandOptionTypes.SubCommandGroup` as type. + +Commands can be + +- Without subcommands / subcommand groups +- With subcommands, but not subcommand groups +- With subcommand groups that have subcommands + +Discord however has a few rules on how we can declare our subcommands + +- Groups can only be 1 in depth, so having a subcommand group inside a subcommand group is not allowed. +- Subcommands can't have subcommand groups as child, only subcommand groups can have subcommands as children +- If you add a subcommand/subcommand group you can not longer use the top-level command, so you can't run the `/roles` command + +For more information on what Discord considers valid for subcommands you can refer to the [official Discord documentation](https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups) + ```ts export const command: CreateApplicationCommand = { name: 'roles', description: 'Role management on your server.', + // insert-start options: [ { name: 'reactions', description: 'Manage the role reactions on your server.', - // If you add more subcommand groups in future, this would need to be false - required: true, type: ApplicationCommandOptionTypes.SubCommandGroup, - options: [], }, ], + // insert-end } ``` Now that the `/roles reactions` is complete, we should add the `/roles reactions create` command. +We are doing this by specifying an option of type subcommand to the subcommand group that we created before. This is a command that we can actually run from Discord. + ```ts options: [ { name: 'reactions', description: 'Manage the role reactions on your server.', - // If you add more subcommand groups in future, this would need to be false - required: true, type: ApplicationCommandOptionTypes.SubCommandGroup, + // insert-start options: [ { name: 'create', description: 'Create a reaction role on your server.', - required: false, type: ApplicationCommandOptionTypes.SubCommand, - options: [], }, ], + // insert-end }, ] ``` ### Options For Creating Reaction Role -Fantastic so now `/roles reactions create` is available, we want to add some options to the create subcommand. We will need the user to provide us with the following things: +Now that `/roles reactions create` is available, we want to add some options to the create subcommand. We will need the user to provide us with the following things: 1. Role - The role we give when the user presses the button -2. Emoji - The emoji we will put on the button. -3. Color - The color of the button. -4. Label - An optional label we can add to the label if the user desires. +1. Emoji - The emoji we will put on the button. +1. Color - The color of the button. +1. Label - An optional label we can add to the button if the user desires. + +We can declare these options with an object inside the `options` array of our subcommand. This object requires us to give it a name, a description, and a type for the option. For the type, we now use the `ApplicationCommandOptionTypes.Role` option because we want the user to provide a role (whereas before we used `ApplicationCommandOptionTypes.SubCommandGroup` and `ApplicationCommandOptionTypes.SubCommand` to declare subcommand groups and subcommands). All the other `ApplicationCommandOptionTypes` we use in this guide will require the user to provide a different kind of information. + +Additionally, since we want some of our options to be required to run the command, we can add the `required: true` option. This will make Discord prevent the user from running the command if they don't fill in the option. ```ts { - name: "create", - description: "Create a reaction role on your server.", - required: false, - type: ApplicationCommandOptionTypes.SubCommand, - options: [ - { - required: true, - name: 'role', - description: 'What role would you like to set for this button?', - type: ApplicationCommandOptionTypes.Role, - }, - ] + name: "create", + description: "Create a reaction role on your server.", + type: ApplicationCommandOptionTypes.SubCommand, + // insert-start + options: [ + { + name: 'role', + description: 'What role would you like to set for this button?', + type: ApplicationCommandOptionTypes.Role, + required: true, + }, + ] + // insert-end } ``` -So now we have added an option for the user to provide us with an role to assign/remove from a user when they press the button. Next we will require the user to give us a emoji. +So now we have added an option for the user to provide a role to assign/remove when anyone presses the button. Next we will require the user to provide an emoji. + +The emoji option will be of type `ApplicationCommandOptionTypes.String`, so that the user will be able to provide anything, including an emoji for the button we'll create in the end. This option, just like the role one, will be required. ```ts { - required: true, - name: 'role', - description: 'What role would you like to set for this button?', - type: ApplicationCommandOptionTypes.Role, + name: 'role', + description: 'What role would you like to set for this button?', + type: ApplicationCommandOptionTypes.Role, + required: true, }, +// insert-start { - required: true, - name: "emoji", - description: "What would you like to set as this button's emoji?", - type: ApplicationCommandOptionTypes.String, + name: "emoji", + description: "What would you like to set as this button's emoji?", + type: ApplicationCommandOptionTypes.String, + required: true, }, +// insert-end ``` -Next let's request a user to provide the button color. +Next let's require the user to provide the button color. + +The button color will be a little different. Until now, we have created options that give the user full control over the value (even if it's invalid), but for the button color, only a few values are actually valid and it's not obvious to the user. For this reason, we can use the `choices` array to create a handful of predefined options that the user can select from. For each options we can decide a `name` that the user will see and a `value` that Discord will send us when the user selects that option. + +We will use the option type `ApplicationCommandOptionTypes.Integer`, but for the `value`, instead of using numbers like `1`, let's pick it from the `ButtonStyles` enum. A number in the code without any label attached to it is known as magic numbers, and it is considered bad practice, which you can read more [here](). + +For example, let's say you write `{ name: "Blue", value: 1 }`. It wouldn't be very clear what button style number 1 is, and you will have to go and check Discord Documentation. Whereas if you write `{ name: "Blue", value: ButtonStyles.Primary }`, it is very obvious that the name "blue" will give us a button with style `Primary` + +Like the other one, this value is required as well. ```ts { - required: true, - name: "emoji", - description: "What would you like to set as this button's emoji?", - type: ApplicationCommandOptionTypes.String, + name: "emoji", + description: "What would you like to set as this button's emoji?", + type: ApplicationCommandOptionTypes.String, + required: true, }, +// insert-start { - required: true, - name: "color", - description: "What color would you like to set as this button's color?", - type: ApplicationCommandOptionTypes.Integer, - choices: [ - { name: "Blue", value: ButtonStyles.Primary }, - { name: "Green", value: ButtonStyles.Success }, - { name: "Grey", value: ButtonStyles.Secondary }, - { name: "Red", value: ButtonStyles.Danger }, - ], + name: "color", + description: "What color would you like to set as this button's color?", + type: ApplicationCommandOptionTypes.Integer, + required: true, + choices: [ + { name: "Blue", value: ButtonStyles.Primary }, + { name: "Green", value: ButtonStyles.Success }, + { name: "Grey", value: ButtonStyles.Secondary }, + { name: "Red", value: ButtonStyles.Danger }, + ], }, +// insert-end ``` -The final option to add to this is the label option. +The final option is the label of the button, and we will use the `ApplicationCommandOptionTypes.String` option type for this. This option is not required, so we can write `required: false` or omit the `required` property. In this guide we will omit the `required` property. ```ts { - required: true, - name: "color", - description: "What color would you like to set as this button's color?", - type: ApplicationCommandOptionTypes.Integer, - choices: [ - { name: "Blue", value: ButtonStyles.Primary }, - { name: "Green", value: ButtonStyles.Success }, - { name: "Grey", value: ButtonStyles.Secondary }, - { name: "Red", value: ButtonStyles.Danger }, - ], + required: true, + name: "color", + description: "What color would you like to set as this button's color?", + type: ApplicationCommandOptionTypes.Integer, + choices: [ + { name: "Blue", value: ButtonStyles.Primary }, + { name: "Green", value: ButtonStyles.Success }, + { name: "Grey", value: ButtonStyles.Secondary }, + { name: "Red", value: ButtonStyles.Danger }, + ], }, +// insert-start { - required: false, - name: "label", - description: "What would you like to set for the name on this button?", - type: ApplicationCommandOptionTypes.String, + name: "label", + description: "What would you like to set for the name on this button?", + type: ApplicationCommandOptionTypes.String, }, +// insert-end ``` Nice. So far your code should look something like this: @@ -215,28 +258,31 @@ const command: CreateApplicationCommand = { { name: 'reactions', description: 'Manage the role reactions on your server.', - // If you add more subcommand groups in future, this would need to be false - required: true, type: ApplicationCommandOptionTypes.SubCommandGroup, options: [ { name: 'create', description: 'Create a reaction role on your server.', - required: false, type: ApplicationCommandOptionTypes.SubCommand, options: [ { + name: 'role', + description: 'What role would you like to set for this button?', + type: ApplicationCommandOptionTypes.Role, required: true, + }, + { name: 'emoji', description: "What would you like to set as this button's emoji?", type: ApplicationCommandOptionTypes.String, + required: true, }, { - required: true, name: 'color', description: "What color would you like to set as this button's color?", type: ApplicationCommandOptionTypes.Integer, + required: true, choices: [ { name: 'Blue', value: ButtonStyles.Primary }, { name: 'Green', value: ButtonStyles.Success }, @@ -245,7 +291,6 @@ const command: CreateApplicationCommand = { ], }, { - required: false, name: 'label', description: 'What would you like to set for the name on this button?', @@ -260,27 +305,29 @@ const command: CreateApplicationCommand = { ``` :::tip -Whenever you write a little bit of code, stop and test to make sure it does what it should before you keep writing more code. +When you write some code, remember to stop and test to make sure it work before continue writing more code. You will make your life harder if you write a lot of code without testing because when you finished, if anything breaks you will have to find what part of the code you wrote is broken. ::: ### Setting Up Slash Creation -Now, we should take a minute to test this code out. However, this code as is does nothing it is just a file that exports an object. Let's make it so that whenever we start our bot, it will create this command for us on our test server. Go back to your index file where you created your bot. +Now, we should take a minute to test this code. Right now, this code does nothing because it's just a file that exports an object. Let's make it so we can create this command on our test server. Create a file named `src/register-commands.ts`. + +An alternative to creating the commands in a test server is to create them in all servers (globally), which be accomplished by using the `upsertGlobalApplicationCommands`. In this guide we will create our commands only in a test server. + +We will also need to import our `command` object defined in the `src/commands/roles.ts` that contains the data for Discord to register our command. ```ts -await bot.rest.upsertGuildApplicationCommands('1234', [roles]) +import { bot } from './index.js' +import roles from './commands/roles.js' -logger.info(`[Startup] Starting bot.`) -await bot.start() +// insert-next-line +const guildId = 'REPLACE WITH YOUR GUILD ID' + +// insert-next-line +await bot.rest.upsertGuildApplicationCommands(guildId, [roles]) ``` -Once you have added that line above, you need to make 2 small changes. The first change is to stop TypeScript from warning you that `roles` does not exist. Let's import roles at the top of the file - -```ts -import roles from './src/commands/roles.js' -``` - -The second thing is that we need to replace the `1234` with your server's guild id where you will be testing. This will make it so that we update any commands whenever the bot is started. +Now the only thing that you need to change is to use your actual server's guild ID in the `guildId` variable. All you need to do is replace the `REPLACE WITH YOUR GUILD ID` with your actual server ID that you will use for testing. This will make the bot automatically update every command whenever the bot is started. ### Cleaner Code @@ -291,38 +338,52 @@ import { CreateApplicationCommand } from '@discordeno/types' import roles from './roles.js' export const commands = new Map( - [roles].map(cmd => [cmd.name, cmd]) + [roles].map(cmd => [cmd.name, cmd]), ) export default commands ``` -Now back in your index file with your bot, let's make use of this map. +:::info +In this guide we manually import the file and add it to the map, but you can get creative and structure the command discovery in any way you like. For example, you could list all the files in a folder, then after verifying that they export a command, add it to this commands map; or you could rely on the files to call a function that will add them to the map; or you can import the map in the file and add it from there. +::: + +Now every time you want to add a new command to your bot you can add it in the array together with the `roles` array and it will be stored in the `commands` map. + +Going back to `src/register-commands.ts` we now need to use the map we just created, so let's import it and use it. ```ts -// This id should reflect your server id by now -await bot.rest.upsertGuildApplicationCommands('1234', [...commands.values()]) +import { bot } from './index.js' +// remove-next-line +import roles from './commands/roles.js' +// insert-next-line +import commands from './commands/index.js' + +// By now this variable should have your server guild id instead of REPLACE WITH YOUR GUILD ID +const guildId = 'REPLACE WITH YOUR GUILD ID' + +// remove-next-line +await bot.rest.upsertGuildApplicationCommands(guildId, [roles]) +// insert-next-line +await bot.rest.upsertGuildApplicationCommands(guildId, [...commands.values()]) ``` -Also make sure to change the import at the top of the file. - -```diff -- import roles from "./src/commands/roles.js"; -+ import commands from "./src/commands/index.js"; -``` - -Go ahead and start your bot, you will see the command is available on your server by typing `/roles reactions create`. If you try and execute the command it will fail since we have not yet added the handling of this command. +Go ahead and start your bot. You should see the command is available on your server by typing `/roles reactions create`. If you try and execute the command, it will fail since we have not write any code handling the command ## Command Execution Handling -Let's make 2 very small files first. `src/events/index.ts` and `src/events/interactionCreate.ts`. Go to the `interactionCreate` file first. +We now need to handle the data that Discord send us when a user types the command. To do this, we can add an event listener on the `interactionCreate` event. + +Create the `src/events/interactionCreate.ts` file so that we can add a new event to handle the data. In here we need to check that the interaction type is a command. This is necessary because interactions can be commands, modals, buttons, commands ran from right-clicking on a user's profile (context menus), and so on. We also need to verify that this interaction has a data object or we won't able to get the name of the command the user wants to run. + +We need to parse all the options Discord has provided us. In the `/roles reactions create` example, we defined a few options, and now we need to get them from the interaction object. This can be accomplished by using the `commandOptionsParser` helper that Discordeno provides. ```ts import commands from '../commands/index.js' -import { commandOptionsParser } from '@discordeno/utils' +import { commandOptionsParser } from '@discordeno/bot' export const event: EventHandlers['interactionCreate'] = async function ( - interaction + interaction, ) { if (interaction.type === InteractionTypes.ApplicationCommand) { if (!interaction.data) return @@ -335,115 +396,1111 @@ export const event: EventHandlers['interactionCreate'] = async function ( } ``` -At this point, we are seeing an error from TypeScript, that the `command` does not have an `.execute()` handler. To add this we need to customize our command just a little bit. Let's make a interface for a custom Command object. Go to `src/commands/index.ts` +Now we need to create the `src/events/index.ts` file to collect all of our events and give it to the bot object. + +```ts +import type { EventHandlers } from '@discordeno/bot' +import { event as interactionCreateEvent } from './interactionCreate.js' + +export const events = { + interactionCreate: interactionCreateEvent, +} as Partial + +export default events +``` + +In this file we can add all of the events we want. In this guide we only need the `interactionCreate` event, but there some other events that might be useful to you, for example the `ready` event, which fires when the bot has established a connection with Discord's gateway. + +To tell Discordeno to run the events, we need another change. Go back to the `src/index.ts` file. + +```ts +import { createBot } from '@discordeno/bot' +import { config } from 'dotenv' + +import events from './events/index.js' + +config() + +const bot = createBot({ + token: process.env.TOKEN, + // remove-next-line + events: {}, + // in this line we only use `events` but for javascript this will traduce to `events: events` + // insert-next-line + events, +}) + +// ... REST OF THE FILE ... +``` + +If we go to the `src/events/interactionCreate.ts` file, we'll see an error from TypeScript. It is telling us that the `command` does not have an `.execute()` handler. To fix this, we need to customize our command just a little bit. Let's make an interface for a custom Command object and use it for our map. Go to the `src/commands/index.ts` file ```ts import { CreateApplicationCommand, Interaction } from '@discordeno/types' import roles from './roles.js' +// remove-start export const commands = new Map( - [roles].map(cmd => [cmd.name, cmd]) + [roles].map(cmd => [cmd.name, cmd]), ) +// remove-end +// insert-start +export const commands = new Map( + [roles].map(cmd => [cmd.name, cmd]), +) +// insert-end export default commands +// insert-start export interface Command extends CreateApplicationCommand { /** Handler that will be executed when this command is triggered */ execute(interaction: Interaction, args: Record): Promise } +// insert-end ``` -Once this interface is made, we should edit the Map to use this as well. - -```ts -export const commands = new Map([ -``` - -Next we should edit the command file at `src/commands/role.ts` and edit it to have an execute handler. +Next, we should edit the command file at `src/commands/role.ts` to have an execute handler. ```ts import { + // remove-next-line CreateApplicationCommand, ApplicationCommandOptionTypes, ButtonStyles, } from '@discordeno/types' +// insert-next-line +import type { Command } from './index.js' -const command: CreateApplicationCommand = { +// Change from 'CreateApplicationCommand' to 'Command' +const command: Command = { name: 'roles', description: 'Role management on your server.', options: [ - // Lot's of options here... + // All the options we defined before ], async execute(interaction, args) {}, } ``` -Once the execute handler is added, make sure to change the type to Command as well. - -```ts -import { CreateApplicationCommand, ApplicationCommandOptionTypes, ButtonStyles } from '@discordeno/types'; -import { Command } from './index.js'; - -const command: Command = { -``` - -Now that this is complete we should go ahead and a type for args so we can get some nice autocomplete when we code. +Now that it's finished, let's add a type for args so we can get some nice autocomplete when we code. ```ts +// Add the 'CommandArgs' for typescript async execute(interaction, args: CommandArgs) { - // Create a reaction role - if (args.create) { - - } + // Create a reaction role + if (args.reactions?.create) { + } } // Place this somewhere at the bottom or top of the file. // Make sure to import all the following types as well. interface CommandArgs { + reactions?: { create?: { - role: Role; - emoji: string; - color: ButtonStyles; - label?: string; - }; + role: Role + emoji: string + color: ButtonStyles + label?: string + } + } } ``` -Finally, we can begin writing the code to handle our commands. Let's start with the `create` command. +Finally, we can begin writing code to handle the `/roles` command. Let's implement the `create` subcommand. In this command the user will find themself in a menu with 3 options + +1. Add another reaction role button +1. Remove an exiting reaction role button +1. Save the current buttons. + +First let's start with creating the menu and the "preview" message of the reaction roles: ```ts +import { ApplicationCommandOptionTypes, ButtonStyles, MessageComponentTypes } from '@discordeno/types' + +// your other code here + async execute(interaction, args: CommandArgs) { - // Create a reaction role - if (args.create) { - const components = new Components() - .addButton( - args.reactions.create.label ?? '', - args.reactions.create.color, - `reactionRole-${args.reactions.create.role.id}`, + // Create a reaction role + if (args.reactions?.create) { + // Send the message that uses will use to get the role + const roleMessage = await bot.helpers.sendMessage(interaction.channelId, { + content: 'Pick your roles', + components: [ + { + type: MessageComponentTypes.ActionRow, + components: [ { - emoji: args.reactions.create.emoji, - }, - ) + type: MessageComponentTypes.Button, + style: args.reactions.create.color, + emoji: { + name: args.reactions.create.emoji, + }, + label: args.reactions.create.label, + customId: `reactionRoles-role-${args.reactions.create.role.id}`, + } + ] + } + ], + }) - await interaction.respond({ - content: 'Use the buttons below to edit the message above. If you need help learning how to edit, press the button below.', - components, - }) + await interaction.respond( + { + content: 'Use the buttons in this message to edit the message below.', + components: [ + { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.Button, + style: ButtonStyles.Success, + customId: 'reactionRoles-add', + emoji: { + name: '➕', + }, + label: 'Add', + }, + { + type: MessageComponentTypes.Button, + style: ButtonStyles.Danger, + customId: 'reactionRoles-remove', + emoji: { + name: '➖', + }, + label: 'Remove', + }, + { + type: MessageComponentTypes.Button, + style: ButtonStyles.Success, + customId: 'reactionRoles-save', + emoji: { + name: '✅', + }, + label: 'Save', + }, + ], + } + ], + }, + { isPrivate: true } + ) + } +} - const message = await bot.rest.getOriginalInteractionResponse(interaction.token) - if (!message) return await interaction.respond('❌ The message was not able to be sent. Cancelling.', { private: true }) +// THE REST OF YOUR CODE +``` - const editComponents = new Components() - .addButton('Add', ButtonStyles.Primary,`reactionRoleAdd-${message.id}`, { emoji: '➕' }) - .addButton('Remove', ButtonStyles.Primary,`reactionRoleRemove-${message.id}`, { emoji: '➖' }) - .addButton('Edit', ButtonStyles.Primary,`reactionRoleEdit-${message.id}`, { emoji: '🖊️' }) - .addButton('Save', ButtonStyles.Success,`reactionRoleSave`, { emoji: '✅' }) - .addButton('Need Help?', ButtonStyles.Link,`https://discord.gg/${BOT_SERVER_INVITE_CODE}`,) +In this piece of code we are doing 2 things: - return await interaction.respond({ - content: 'ROLES_REACTIONS_CREATE_PLACEHOLDER_EDIT', - components: editComponents, - }) - } +- Send a message to the channel where the command was run, with a button everyone can click to add/remove the roles that the user who run this command has set up. +- Send a private message so that only the user who run the command can see it and allow them to edit the message we just sent. + +Since we never created an actual button before, let's try to understand the properties in these objects. + +Let's start by trying to understand the object we pass to `sendMessage` that allows us to define what data the message should have. A message has a content string and a component array. The component array can have up to 5 action rows inside of it. Each action row can have 1 select menu, 1 input text, or up to 5 buttons. Later we will discuss what a select menu and input text is as well as how to use them. For now, let's focus on buttons. We can recognize them by the `type: MessageComponentTypes.Button`, and it has a different requirement comparing to action rows. Buttons need to have: + +- A `style` - How Discord should display them, +- A `label` or `emoji` - What Discord should display as the text in the buttons +- A `customId` - Developer defined ID that can be up to 100 characters long. This is where we can store information and use it to tell the buttons apart from one another. We will see how this is useful soon + +This also applies to the `interaction.respond` function that we call. It too has a `content` for the message and a `components` array, inside of which is an action row containing 3 buttons that we defined. + +If you save and then run the bot, you might noticed that Discord still says that the application did not respond, but how is that possibile? + +Although we just added the code to respond to the interaction, we have forgot a Discordeno concept called `desired properties`. This is an optimization Discordeno uses to make your code more performant but can be found annoying or unnecessary. + +To explain how the `desired properties` work we need to talk about how Discord sends us data. Discord uses its own way to require/give data to who consumes the API. This guide won't go deep into this, but if you are interested can refer to the official documentation. + +The way Discord sends us data is not the way that we (might) want it and for that reason Discordeno needs to map it from the Discord format to the Discordeno format. This is done via `transformers` defined in the `bot.transformers` object to tell Discordeno what we need from the pile of data Discord provides us. + +Looking through the code we have written so far we can see that + +- We use `interaction.type` and `interaction.data` in the `src/events/interactionCreate.ts` file. +- We use `interaction.channelId`, `interaction.id`, `interaction.token`, and `role.id` in our command. + +We need to add all of properties that we use to the `desired properties` list, and to do so we go back to `src/index.ts` and add a few lines: + +```ts +// REST OF YOUR CODE + +const bot = createBot({ + token, + events, +}) + +// insert-start +bot.transformers.desiredProperties.interaction.id = true +bot.transformers.desiredProperties.interaction.data = true +bot.transformers.desiredProperties.interaction.type = true +bot.transformers.desiredProperties.interaction.token = true +bot.transformers.desiredProperties.interaction.channelId = true + +bot.transformers.desiredProperties.role.id = true +// insert-end + +// REST OF YOUR CODE +``` + +:::tip +As said before the code you are creating is your code, and in being so you can structure it how you find it better for you. This means that if you don't like having to specify the `bot.transformers.desiredProperties` lines in your index.ts nothing is preventing you from moving them somewhere else and make your code call them in a way or another, a way is for example moving to a file apart and creating a function that will edit all the values. +::: + +:::note +If you want, you can disable the `desired properties` with the `defaultDesiredPropertiesValue` option in the createBot object, it isn't recommended but it's there to allow the developer to choose, keep in mind this will give you a warning in the console when you run the bot and memory/cpu usage WILL be higher. +::: + +If we try again now we'll finally see our message with 3 buttons. But if we click any of the buttons, they don't do anything! This is expected, since we did not write any code to handle buttons. So let's talk about how to react to users' interactions beyond just commands + +### Handling interaction beyond commands + +Discord is saying that we aren't responding to the button click, so we still need to make more changes to our command and event files. The problem is rather simple: we need a way to get the interaction of the user clicking, for example, the interaction of that "Add" button after the user uses the command. + +Interactions do not "chain", but they do have some data we can use to connect them. In this case we can use the `message` property, (which is defined if the interaction Discord sent us comes from a user interacting with a message component, like a button). This seems perfect, we now have a way to tell whether or not we need to do something with an interaction. We still face an issue: **how** do we get the data from the interaction of the button click in our command? + +You can get creative and do what you think is the most appropriate thing to handle this situation, a few examples are: + +- `Collectors` - What we will use in this example guide. +- Manual check of the customId in the global event. + +To explain what a `collector` is, let's take an array as an example. In an array, we can read, add, and remove items. So what if we had a structure which can "collect" items, and notify us when it collects an item we actually need? This sounds like it solves our issue, so let's implement this Collector class. In this guide we will write it in a very simple way, but there are enhancements you might want to add, which we will discuss later. + +Let's create a `src/collector.ts` file. We will be using NodeJS's `EventEmitter` feature to create this Collector class. We won't explain what an event emitter is, just know it makes your life easier to create this: + +```ts +import { EventEmitter } from 'node:events' +import { Interaction } from '@discordeno/bot' + +export class ItemCollector extends EventEmitter { + onItem(callback: (item: Interaction) => unknown): void { + this.on('item', callback) + } + + collect(item: Interaction): void { + this.emit('item', item) + } +} + +export default class ItemCollector +``` + +:::info +If you are following along using Bun or Deno you can use this code as well even if we use a node specific feature, this is because Bun supports (almost) all apis from node, and deno supports a lot of them in the latest versions, so if you are getting an error on the `EventEmitter` you might need to update your bun/deno +::: + +If we now go to our `src/events/interactionCreate.ts` we can add some code: + +```ts +// REST OF YOUR CODE HERE + +// insert-next-line +import ItemCollector from '../collector.js' + +// insert-next-line +export const collectors = new Set() + +export const event: EventHandlers['interactionCreate'] = async interaction => { + // insert-next-line + for (const collector of collectors) { + // insert-next-line + collector.collect(interaction) + // insert-next-line + } + + // REST OF YOUR CODE HERE } ``` + +In here you are defining a `Set` (for what we use, we can see it exactly the same as an array with a few helpful methods) of these collectors and when we receive an interaction from Discord we collect in all the collectors that have been added to then handle the interaction, so if the have just received the button click interaction we will now able to respond to it. + +:::tip +As already said: you don't like how the collection is done in this example? You are free to change it and have fun in experimenting what you find to be the better way +::: + +To do this we need to update the command, `src/events/roles.ts`: + +```ts +// THE REST OF YOUR CODE + +// insert-next-line +import ItemCollector from '../collector.js' +// insert-next-line +import { collectors } from '../events/interactionCreate.js' + +async execute(interaction, args: CommandArgs) { + // Create a reaction role + if (args.reactions?.create) { + // THE REST OF YOUR CODE + + // remove-start + await interaction.respond( + { + // all the options we defined earlier + }, + { isPrivate: true } + ) + // remove-end + // insert-start + await interaction.defer(true); + const message = await interaction.respond( + { + // all the options we defined earlier + }, + { isPrivate: true } + ) + + const itemCollector = new ItemCollector() + collectors.add(itemCollector) + + itemCollector.onItem((i) => { + if (i.message?.id !== message.id) { + return + } + + await i.respond("Hello world"); + }) + + // insert-end + } +} + +// THE REST OF YOUR CODE +``` + +In the `onItem` function, we are making sure we are only responding if the message object of the interaction is for this command. This is accomplished by checking the `i.message.id` and comparing it to the id of the message we just sent. But wait, you might think, we don't have the ID of the message we just sent, do we? And you would be right, as of right now, we don't. We need to make a small change: we can get the message object from the return value of `interaction.respond`. We also need to add an `interaction.defer` before `interaction.respond`. We won't go into details as for why we need to do this, just know it is due to how Discord interactions work. + +:::warning +A mis-use of the interaction from the code that uses these interaction by using the ItemCollector can lead to unexpected behavior, so make sure to check the interaction "nature" before using it, like in our example by making sure it is related to our message. +::: + +You might remember from before that we discussed the desired properties, and we now need to update them, we are now using `i.message` and we are using the `.id` of a message (`i.message.id`) so we need to add 2 lines to our list in the `src/index.ts`: + +```ts +// REST OF YOUR CODE + +// insert-next-line +bot.transformers.desiredProperties.message.id = true + +bot.transformers.desiredProperties.interaction.id = true +bot.transformers.desiredProperties.interaction.data = true +bot.transformers.desiredProperties.interaction.type = true +bot.transformers.desiredProperties.interaction.token = true +// insert-next-line +bot.transformers.desiredProperties.interaction.message = true +bot.transformers.desiredProperties.interaction.channelId = true + +// REST OF YOUR CODE +``` + +If we run the code at this point we can see that by clicking the button we will get a message back saying `Hello world`, we did it! We responded to a button from inside the command! + +### Handling the menu + +We now need to handle correctly the 3 buttons we declared before, as mentioned before Discord allows us to declare custom ids we can reference in our code, so let's start with that: + +First we can implement the easiest buttons out of the 3, the save button. Since we are going to "live" edit a message after our menu message, we just need to delete the menu message, then we can remove the collector since we don't need it anymore + +```ts +itemCollector.onItem(async i => { + if (i.message?.id !== message.id) { + return + } + + // remove-next-line + await i.respond('Hello world') + + // insert-start + if (i.data?.customId === 'reactionRoles-save') { + collectors.delete(itemCollector) + + await i.deferEdit() + await i.delete() + + return + } + // insert-end + + // REST OF YOUR CODE +}) + +// REST OF YOUR CODE +``` + +If we now try to click the save button, the menu will close, so we are done! + +Let's move on the add and remove button. We have a new problem: we need a way to update the buttons shown in the final message, and we also need to know what reaction buttons the user has created up to this point. + +Let's start with the second issue. We can store in an array all the buttons the user created: + +```ts +// insert-next-line +let roles = [args.reactions.create] + +const roleMessage = await bot.helpers.sendMessage(interaction.channelId, { + content: 'Pick your roles', + components: [ + { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.Button, + style: args.reactions.create.color, + emoji: { + name: args.reactions.create.emoji, + }, + label: args.reactions.create.label, + customId: `reactionRoles-role-${args.reactions.create.role.id}`, + }, + ], + }, + ], +}) +``` + +Now we can deal with the first problem. We need to have something to create these button objects. We can do this by creating a pretty easy function that will: + +- Create an action row array +- Add an action row if we have some buttons to add +- If we have reached the limit imposed by Discord (5 buttons per action row) it creates another action row and start using that + +```ts +function getRoleButtons( + roles: Array<{ + role: Role + emoji: string + color: ButtonStyles + label?: string | undefined + }>, +): ActionRow[] { + const actionRows: ActionRow[] = [] + + if (roles.length === 0) return actionRows + + // We add the components later, so we need to make typescript know that we are sure that it will be a compatible components array + actionRows.push({ + type: MessageComponentTypes.ActionRow, + components: [] as unknown as ActionRow['components'], + }) + + for (const roleInfo of roles) { + let actionRow = actionRows.at(-1) + + if (!actionRow) { + throw new Error('Unable to get actionRow') + } + + if (actionRow.components.length === 5) { + actionRow = { + type: MessageComponentTypes.ActionRow, + components: [] as unknown as ActionRow['components'], + } + actionRows.push(actionRow) + } + + actionRow?.components.push({ + type: MessageComponentTypes.Button, + style: roleInfo.color, + emoji: { + name: roleInfo.emoji, + }, + label: roleInfo.label, + customId: `reactionRoles-role-${roleInfo.role.id}`, + }) + } + + return actionRows +} +``` + +:::info +Remember to import all the types we are using. Some IDE/Text editors will offer an option to quickly fix the errors about the types not being found and import them +::: + +Now we can use this function. Let's go back right after we declare the roles array + +```ts +let roles = [args.reactions.create] + +const roleMessage = await bot.helpers.sendMessage(interaction.channelId, { + content: 'Pick your roles', + // remove-start + components: [ + { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.Button, + style: args.reactions.create.color, + emoji: { + name: args.reactions.create.emoji, + }, + label: args.reactions.create.label, + customId: `reactionRoles-role-${args.reactions.create.role.id}`, + }, + ], + }, + ], + // remove-end + // insert-next-line + components: getRoleButtons(roles), +}) +``` + +Now let's implement the remove button, as it is the next easiest one. To implement this we want to give the user the choice to select between an already exiting reaction roles buttons. To do this we can use a select menu: + +```ts +itemCollector.onItem(async i => { + if (i.message?.id !== message.id) { + return + } + + if (i.data?.customId === 'reactionRoles-save') { + collectors.delete(itemCollector) + + await i.deferEdit() + await i.delete() + + return + } + + // insert-start + if (i.data?.customId === 'reactionRoles-remove') { + const options: SelectOption[] = [] + + for (const roleInfo of roles) { + options.push({ + label: `${roleInfo.emoji} ${roleInfo.label}`, + value: roleInfo.role.id.toString(), + }) + } + + await i.deferEdit() + await i.edit({ + content: 'Select what reaction role to remove', + components: [ + { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.SelectMenu, + customId: 'reactionRoles-remove-selectMenu', + maxValues: 1, + minValues: 1, + placeholder: 'Select roles', + options, + }, + ], + }, + ], + }) + + return + } + // insert-end + + // REST OF YOUR CODE +}) + +// REST OF YOUR CODE +``` + +And now we need to add the code for the select menu: + +```ts +if (i.data?.customId === 'reactionRoles-remove') { + const options: SelectOption[] = [] + + for (const roleInfo of roles) { + options.push({ + label: `${roleInfo.emoji} ${roleInfo.label}`, + value: roleInfo.role.id.toString(), + }) + } + + await i.deferEdit() + await i.edit({ + content: 'Select what reaction role to remove', + components: [ + { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.SelectMenu, + customId: 'reactionRoles-remove-selectMenu', + maxValues: 1, + minValues: 1, + placeholder: 'Select roles', + options, + }, + ], + }, + ], + }) + + return +} + +// insert-start +if (i.data?.customId === 'reactionRoles-remove-selectMenu') { + const roleToRemove = i.data?.values?.[0] + + await i.deferEdit() + + roles = roles.filter(roleInfo => roleInfo.role.id.toString() !== roleToRemove) + + await bot.helpers.editMessage(interaction.channelId, roleMessage.id, { + components: getRoleButtons(roles), + }) + + await i.edit({ + content: 'Use the buttons in this message to edit the message below.', + components: [messageActionRow], + }) + + return +} +// insert-end + +// REST OF YOUR CODE +``` + +And now we are left just one thing, the add button. For this we now need to use a new type of interaction responses: modals + +Modals are popups that we can create to require the user to input something, for example the emoji and (optionally) the label. To use them with the `interaction.respond` method we can add a `title` (a required property by modals) to the objects. + +Other than emoji and labels, we also need the role to give and the color for the button. Unfortunately we can't add them directly in our modal, Discord does not allow it, so we need to find another way. We can + +1. Wait for the button click on the add button +1. Show the user a select menu for the role +1. Show the user a select menu but for the color this time +1. Show the user the modal for the emoji and label +1. Create our new button + +Since it's a multi-step process, we need to store the partial data of this new role, so let's start with that. We can add it right before our onItem call: + +```ts +// REST OF YOUR CODE + +// insert-next-line +let partialRoleInfo: Partial<(typeof roles)[number]> | undefined + +itemCollector.onItem(async i => { + // REST OF YOUR CODE +}) + +// REST OF YOUR CODE +``` + +Now we can start with the code for the button click and the 2 select menu as we already know how that code looks like: + +```ts +if (i.data?.customId === 'reactionRoles-remove-selectMenu') { + const roleToRemove = i.data?.values?.[0] + + await i.deferEdit() + + roles = roles.filter(roleInfo => roleInfo.role.id.toString() !== roleToRemove) + + await bot.helpers.editMessage(interaction.channelId, roleMessage.id, { + components: getRoleButtons(roles), + }) + + await i.edit({ + content: 'Use the buttons in this message to edit the message below.', + components: [messageActionRow], + }) + + return +} + +// insert-start +if (i.data?.customId === 'reactionRoles-add') { + await i.deferEdit() + + partialRoleInfo = {} + + await i.edit({ + content: 'Pick a role for the new reaction role', + components: [ + { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.SelectMenuRoles, + customId: 'reactionRoles-add-role', + maxValues: 1, + minValues: 1, + placeholder: 'Select a role', + }, + ], + }, + ], + }) + return +} + +if (i.data?.customId === 'reactionRoles-add-role') { + const roleToAdd = i.data?.resolved?.roles?.first() + + partialRoleInfo.role = roleToAdd + + await i.deferEdit() + await i.edit({ + content: 'Pick a color for the reaction role', + components: [ + { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.SelectMenu, + customId: 'reactionRoles-add-color', + options: [ + { label: 'Blue', value: ButtonStyles.Primary.toString() }, + { label: 'Green', value: ButtonStyles.Success.toString() }, + { label: 'Grey', value: ButtonStyles.Secondary.toString() }, + { label: 'Red', value: ButtonStyles.Danger.toString() }, + ], + }, + ], + }, + ], + }) + + return +} + +if (i.data?.customId === 'reactionRoles-add-color') { + const color = parseInt(i.data?.values?.[0]) + + partialRoleInfo.color = color + + await i.respond({ + content: 'Hello world', + }) + + return +} +// insert-end + +// REST OF YOUR CODE +``` + +Now we're only missing the modal part. For now, we'll just put a "hello world" to verify that everything is working. To create a modal we need: + +- A `title` - the title for the modal that the user will see +- A `components` array - Like for messages modals require us to give a action rows +- A `customId` - to identify the modal that the user submitted + +```ts +if (i.data?.customId === 'reactionRoles-add-color') { + const color = parseInt(i.data?.values?.[0]) + + partialRoleInfo.color = color + + // remove-start + await i.respond({ + content: 'Hello world', + }) + // remove-end + // insert-start + await i.respond({ + title: 'Pick an emoji and label for the reaction role', + components: [ + { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.InputText, + style: TextStyles.Short, + customId: 'reactionRoles-add-emoji', + label: 'Emoji for the reaction role', + required: true, + }, + ], + }, + { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.InputText, + style: TextStyles.Short, + customId: 'reactionRoles-add-label', + label: 'Label for the reaction role [OPTIONAL]', + }, + ], + }, + ], + customId: 'reactionRoles-add-modal', + }) + // insert-end + + return +} +``` + +You might notice that we are using a new type of message component, the input text. These are just text field the user needs to fill. It have 2 styles, short (the one that we are using in this case) and paragraph. The only difference is that an input text of style paragraph is designed to accept a longer string. + +Now we just need to handle the modal interaction and we will be done with the menu. + +```ts +if (i.data?.customId === 'reactionRoles-add-color') { + const color = parseInt(i.data?.values?.[0]) + + partialRoleInfo.color = color + + await i.respond({ + title: 'Pick an emoji and label for the reaction role', + components: [ + { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.InputText, + style: TextStyles.Short, + customId: 'reactionRoles-add-emoji', + label: 'Emoji for the reaction role', + required: true, + }, + ], + }, + { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.InputText, + style: TextStyles.Short, + customId: 'reactionRoles-add-label', + label: 'Label for the reaction role [OPTIONAL]', + }, + ], + }, + ], + customId: 'reactionRoles-add-modal', + }) + + return +} + +// insert-start +if (i.data?.customId === 'reactionRoles-add-modal') { + const emoji = i.data.components?.[0]?.components?.[0].value + const label = i.data.components?.[1]?.components?.[0].value + + partialRoleInfo.emoji = emoji + partialRoleInfo.label = label + + roles.push(partialRoleInfo) + + await bot.helpers.editMessage(interaction.channelId, roleMessage.id, { + components: getRoleButtons(roles), + }) + + partialRoleInfo = undefined + + await interaction.edit({ + content: 'Use the buttons in this message to edit the message below.', + components: [messageActionRow], + }) + + await i.respond( + 'Reaction role created successfully. You can use the message above to add/remove a role', + { isPrivate: true }, + ) + + return +} +// insert-end +``` + +And with this we are done with the menu. You might see that we are responding to the modal while in every other case we just edited the original message. The reason is that for modals the edit does not count as responding to it and you need to send a new message. + +Finally, let's move to the handling of our role buttons + +## Role buttons Handling + +Let's move back to `src/events/interactionCreate.ts`. We need to add some code after the command handling: + +```ts +if (interaction.type === InteractionTypes.ApplicationCommand) { + if (!interaction.data) return + + const command = commands.get(interaction.data.name) + if (!command) return + + try { + await command.execute(interaction, commandOptionsParser(interaction)) + } catch (error) { + console.error(error) + } +} + +// insert-add +if ( + interaction.type === InteractionTypes.MessageComponent && + interaction.data?.componentType === MessageComponentTypes.Button +) { + if (!interaction.data?.customId?.startsWith('reactionRoles-role-')) return + + await interaction.respond('Hello world') +} +// insert-end +``` + +We are checking what message component interaction type we received, in this case if it's a button. We only need a couple of things from here: + +1. Get the role id of the role we need to give the user +1. Assign it to them +1. Respond to the interaction + +So let's do this: + +```ts +if ( + interaction.type === InteractionTypes.MessageComponent && + interaction.data?.componentType === MessageComponentTypes.Button +) { + if (!interaction.data?.customId?.startsWith('reactionRoles-role-')) return + // insert-start + const roleId = BigInt( + interaction.data.customId.slice('reactionRoles-role-'.length), + ) + + await interaction.bot.helpers.addRole( + interaction.guildId, + interaction.user.id, + roleId, + `Reaction role button for role id ${roleId}`, + ) + await interaction.respond(`I added to you the <@&${roleId}> role.`, { + isPrivate: true, + }) + // insert-end +} +``` + +In this final piece of code, we use some desired properties. Let's go to the `src/index.ts` file and add the last few lines of desired properties! + +```ts +// REST OF YOUR CODE + +// insert-next-line +bot.transformers.desiredProperties.user.id = true + +bot.transformers.desiredProperties.message.id = true + +bot.transformers.desiredProperties.interaction.id = true +bot.transformers.desiredProperties.interaction.data = true +bot.transformers.desiredProperties.interaction.type = true +// insert-next-line +bot.transformers.desiredProperties.interaction.user = true +bot.transformers.desiredProperties.interaction.token = true +bot.transformers.desiredProperties.interaction.message = true +// insert-next-line +bot.transformers.desiredProperties.interaction.guildId = true +bot.transformers.desiredProperties.interaction.channelId = true + +bot.transformers.desiredProperties.role.id = true + +// REST OF YOUR CODE +``` + +If we try to run the code, we will finally achieve what we want: A button that when clicked gives us the role, assuming that Discord did not return an error, which could be caused by Discord not allowing bots to add roles such as `@everyone`, roles created for bot permissions, roles that are obtained with link roles or roles that are above the bot hightest role. + +One last thing we could do is removing the role if we already have it. We will need to add some code in the event and a few desired properties. Let's start with the event file `src/events/interactionCreate.ts`: + +```ts +if ( + interaction.type === InteractionTypes.MessageComponent && + interaction.data?.componentType === MessageComponentTypes.Button +) { + if (!interaction.data?.customId?.startsWith('reactionRoles-role-')) return + const roleId = BigInt( + interaction.data.customId.slice('reactionRoles-role-'.length), + ) + + // remove-start + await interaction.bot.helpers.addRole( + interaction.guildId, + interaction.user.id, + roleId, + `Reaction role button for role id ${roleId}`, + ) + await interaction.respond(`I added to you the <@&${roleId}> role.`, { + isPrivate: true, + }) + // remove-end + // insert-start + const alreadyHasRole = !!interaction.member.roles.find( + role => role === roleId, + ) + + if (alreadyHasRole) { + await interaction.bot.helpers.removeRole( + interaction.guildId, + interaction.user.id, + roleId, + `Reaction role button for role id ${roleId}`, + ) + await interaction.respond(`I removed from you the <@&${roleId}> role.`, { + isPrivate: true, + }) + return + } + + await interaction.bot.helpers.addRole( + interaction.guildId, + interaction.user.id, + roleId, + `Reaction role button for role id ${roleId}`, + ) + await interaction.respond(`I added to you the <@&${roleId}> role.`, { + isPrivate: true, + }) + // insert-end +} +``` + +And now let's add the desired properties. In the `src/index.ts` we need just a few lines: + +```ts +// REST OF YOUR CODE + +bot.transformers.desiredProperties.user.id = true + +bot.transformers.desiredProperties.message.id = true + +// insert-next-line +bot.transformers.desiredProperties.member.roles = true + +bot.transformers.desiredProperties.interaction.id = true +bot.transformers.desiredProperties.interaction.data = true +bot.transformers.desiredProperties.interaction.type = true +bot.transformers.desiredProperties.interaction.user = true +bot.transformers.desiredProperties.interaction.token = true +// insert-next-line +bot.transformers.desiredProperties.interaction.member = true +bot.transformers.desiredProperties.interaction.message = true +bot.transformers.desiredProperties.interaction.guildId = true +bot.transformers.desiredProperties.interaction.channelId = true + +bot.transformers.desiredProperties.role.id = true + +// REST OF YOUR CODE +``` + +If we test the code now, it should work. We just created a reaction role feature! + +## Improvements + +You might remember that we said there could be improvements to the collectors we have. Currently, if the user does not save the menu, we will have that collector class in memory until we restart the bot. This can be easily fixed by having a timeout on the collector, but that is something that you can explore on your own. + +Also a more advanced thing that you can do is to generalize the collectors. We currently use the `Interaction` type for the methods implemented on it but we don't use them in any way. While we could use `any` or `unknown` instead, the best way is to generalize it using generics in typescript, so you can re-use that class without having to create another one. + +In the main file (`src/index.ts`), we currently update the commands on every bot startup even if the commands haven't changed. This may cause you to hit the ratelimit for that API endpoint, especially in development where you might restart a lot your bot. You have a couple of options to fix it, such as moving the api request to another file and run that only when you update your commands. Another options is to check for the exiting commands, check if there are any changes and only then update your commands. + +You could also move the various Discord objects to the bottom of the file and make them act like template if and when needed, but that is not a functional improvement but rather a maintainability one. + +Also currently there are a few cases where this code could error. In fact, if you have a strict typescript configuration enabled, you might have noticed that typescript is giving you errors all over the place, especially in our command because stuff can be `undefined` and we don't check for it. To fix this you just need to add an `if` statement to ensure they exist, and if not, just return. + +Also, the code will throw an error if there's no button and the user click the remove button, or if there are 25 buttons and the user click the add button. + +A few of these things, to be exact the last 3, are implemented in the full example code you can find over the github repo [`/example/reaction-roles`](https://github.com/discordeno/discordeno/blob/main/examples/reaction-roles) folder that has the entire project for this guide. diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 9c5b0381c..05d5611bf 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -219,6 +219,24 @@ const config: Config = { prism: { theme: themes.github, darkTheme: themes.dracula, + magicComments: [ + // Docusaurus default magic comment + { + className: 'theme-code-block-highlighted-line', + line: 'highlight-next-line', + block: { start: 'highlight-start', end: 'highlight-end' }, + }, + { + className: 'theme-code-block-add', + line: 'insert-next-line', + block: { start: 'insert-start', end: 'insert-end' }, + }, + { + className: 'theme-code-block-remove', + line: 'remove-next-line', + block: { start: 'remove-start', end: 'remove-end' }, + }, + ], additionalLanguages: ['bash'], }, } satisfies ThemeConfig, diff --git a/website/src/styling/index.css b/website/src/styling/index.css index 9a3f692f4..52718a2fe 100644 --- a/website/src/styling/index.css +++ b/website/src/styling/index.css @@ -36,3 +36,43 @@ --ifm-footer-link-color: #cfcfcf; --ifm-footer-background-color: #1c1c1c; } + +.theme-code-block-add { + --border-left: 10px; + + position: relative; + background-color: #08ff0020; + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 calc(var(--ifm-pre-padding)); +} + +.theme-code-block-add::before { + position: absolute; + left: 4px; + + content: '+'; + + color: #00da86ce; + font-weight: bold; +} + +.theme-code-block-remove { + --border-left: 10px; + + position: relative; + background-color: #ff000020; + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 calc(var(--ifm-pre-padding)); +} + +.theme-code-block-remove::before { + position: absolute; + left: 4px; + + content: '-'; + + color: #da001dce; + font-weight: bold; +}