diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index f64aa2388402..000000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,12 +0,0 @@ -**Please describe the changes this PR makes and why it should be merged:** - -**Status and versioning classification:** - - diff --git a/.github/workflows/cleanup-cache.yml b/.github/workflows/cleanup-cache.yml index 8d34cc93634f..4a4ec06541b3 100644 --- a/.github/workflows/cleanup-cache.yml +++ b/.github/workflows/cleanup-cache.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Cleanup caches run: | diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index d23bce7ad884..3d305eb186fb 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -14,7 +14,7 @@ jobs: if: github.repository_owner == 'discordjs' steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Node.js v24 uses: actions/setup-node@v6 diff --git a/.github/workflows/deprecate-version.yml b/.github/workflows/deprecate-version.yml index f16ef54a4fed..26f7efe69a9e 100644 --- a/.github/workflows/deprecate-version.yml +++ b/.github/workflows/deprecate-version.yml @@ -36,7 +36,7 @@ jobs: if: github.repository_owner == 'discordjs' steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Node.js v24 uses: actions/setup-node@v6 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index b61f566b2098..429f79483aef 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -38,7 +38,7 @@ jobs: if: github.repository_owner == 'discordjs' steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ inputs.ref || '' }} @@ -56,7 +56,7 @@ jobs: - name: Checkout main repository if: ${{ inputs.ref && inputs.ref != 'main' }} - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: 'main' @@ -213,7 +213,7 @@ jobs: if: github.repository_owner == 'discordjs' steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Node.js v24 uses: actions/setup-node@v6 diff --git a/.github/workflows/label-sync.yml b/.github/workflows/label-sync.yml index 15ee80bacec1..6b3d959981f7 100644 --- a/.github/workflows/label-sync.yml +++ b/.github/workflows/label-sync.yml @@ -15,7 +15,7 @@ jobs: if: github.repository_owner == 'discordjs' steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Label sync uses: crazy-max/ghaction-github-labeler@v5 diff --git a/.github/workflows/publish-dev-docker.yml b/.github/workflows/publish-dev-docker.yml index 11c37e4319c0..550b190f28de 100644 --- a/.github/workflows/publish-dev-docker.yml +++ b/.github/workflows/publish-dev-docker.yml @@ -3,29 +3,88 @@ on: schedule: - cron: '0 */12 * * *' workflow_dispatch: +env: + IMAGE_NAME: discordjs/proxy jobs: - docker-publish: - name: Docker publish - runs-on: ubuntu-latest - if: github.repository_owner == 'discordjs' + build: + name: Build ${{ matrix.platform }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm steps: + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_ACCESS_TOKEN }} - - name: Install Node.js v24 - uses: actions/setup-node@v6 + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 with: - node-version: 24 - package-manager-cache: false + context: . + file: packages/proxy-container/Dockerfile + platforms: ${{ matrix.platform }} + outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - - name: Install dependencies - uses: ./packages/actions/src/pnpmCache + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v5 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + name: Create and push manifest list + runs-on: ubuntu-latest + needs: build + steps: + - name: Download digests + uses: actions/download-artifact@v6 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - run: echo ${{ secrets.DOCKER_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create -t ${{ env.IMAGE_NAME }}:latest \ + $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) - - name: Build & push docker image - run: docker build -f packages/proxy-container/Dockerfile -t discordjs/proxy:latest --push . + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:latest diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml index 58c675a5c3ea..c63b44b040a4 100644 --- a/.github/workflows/publish-dev.yml +++ b/.github/workflows/publish-dev.yml @@ -34,7 +34,7 @@ jobs: private-key: ${{ secrets.DISCORDJS_APP_KEY_RELEASE }} - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ steps.app-token.outputs.token }} @@ -55,7 +55,7 @@ jobs: - name: Checkout main repository (non-main ref) if: ${{ inputs.ref != 'main' }} - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: 'main' diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 5af3a4b05bcf..5b29d28e4ee6 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -1,28 +1,98 @@ name: Publish docker images on: workflow_dispatch: +env: + IMAGE_NAME: discordjs/proxy jobs: - docker-publish: - name: Docker publish - runs-on: ubuntu-latest + build: + name: Build ${{ matrix.platform }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm steps: + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Install Node.js v24 - uses: actions/setup-node@v6 + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 with: - node-version: 24 - package-manager-cache: false + context: . + file: packages/proxy-container/Dockerfile + platforms: ${{ matrix.platform }} + outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - - name: Install dependencies - uses: ./packages/actions/src/pnpmCache + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v5 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + name: Create and push manifest list + runs-on: ubuntu-latest + needs: build + steps: + - name: Download digests + uses: actions/download-artifact@v6 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Get Major Version + id: version + run: | + FULL_VER=$(jq --raw-output '.version' packages/proxy-container/package.json) + MAJOR=$(echo $FULL_VER | cut -d '.' -f1) + echo "major=$MAJOR" >> $GITHUB_OUTPUT - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - run: echo ${{ secrets.DOCKER_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create -t ${{ env.IMAGE_NAME }}:${{ steps.version.outputs.major }} \ + $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) - - name: Build & push docker image - run: docker build -f packages/proxy-container/Dockerfile -t discordjs/proxy:$(cut -d '.' -f1 <<< $(jq --raw-output '.version' packages/proxy-container/package.json)) --push . + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ steps.version.outputs.major }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3faf8c33f96..092824237c51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,7 +55,7 @@ jobs: permission-contents: write - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: token: ${{ steps.app-token.outputs.token }} ref: ${{ inputs.ref || '' }} @@ -75,7 +75,7 @@ jobs: - name: Checkout main repository if: ${{ inputs.ref && inputs.ref != 'main' }} - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: 'main' diff --git a/.github/workflows/remove-tag.yml b/.github/workflows/remove-tag.yml index 55987b88d8d7..0cfca401d567 100644 --- a/.github/workflows/remove-tag.yml +++ b/.github/workflows/remove-tag.yml @@ -35,7 +35,7 @@ jobs: fail-fast: false steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 67581de9f672..4e2e0a904879 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: TURBO_TEAM: ${{ vars.TURBO_TEAM }} steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/upload-readmes.yml b/.github/workflows/upload-readmes.yml new file mode 100644 index 000000000000..88f4c4aca8e6 --- /dev/null +++ b/.github/workflows/upload-readmes.yml @@ -0,0 +1,42 @@ +name: Upload README.md files +on: + push: + branches: + - 'main' + paths: + - 'packages/*/README.md' + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true +jobs: + upload-readmes: + name: Upload README.md files + runs-on: ubuntu-latest + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + if: github.repository_owner == 'discordjs' + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + package-manager-cache: false + + - name: Install dependencies + uses: ./packages/actions/src/pnpmCache + + - name: Build dependencies + run: pnpm --filter @discordjs/actions... run build + + - name: Upload README.md files + env: + CF_R2_READMES_ACCESS_KEY_ID: ${{ secrets.CF_R2_READMES_ACCESS_KEY_ID }} + CF_R2_READMES_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_READMES_SECRET_ACCESS_KEY }} + CF_R2_READMES_BUCKET: ${{ secrets.CF_R2_READMES_BUCKET }} + CF_R2_READMES_URL: ${{ secrets.CF_R2_READMES_URL }} + uses: ./packages/actions/src/uploadReadmeFiles diff --git a/apps/guide/content/docs/legacy/interactions/images/modal-example.png b/apps/guide/content/docs/legacy/interactions/images/modal-example.png index 9cee3d67d630..7b494dcd257c 100644 Binary files a/apps/guide/content/docs/legacy/interactions/images/modal-example.png and b/apps/guide/content/docs/legacy/interactions/images/modal-example.png differ diff --git a/apps/guide/content/docs/legacy/interactions/images/selectephem.png b/apps/guide/content/docs/legacy/interactions/images/selectephem.png deleted file mode 100644 index 2f109b358862..000000000000 Binary files a/apps/guide/content/docs/legacy/interactions/images/selectephem.png and /dev/null differ diff --git a/apps/guide/content/docs/legacy/interactions/modals.mdx b/apps/guide/content/docs/legacy/interactions/modals.mdx index 4bdc32bc4a6f..2367dfac7faf 100644 --- a/apps/guide/content/docs/legacy/interactions/modals.mdx +++ b/apps/guide/content/docs/legacy/interactions/modals.mdx @@ -2,30 +2,27 @@ title: Modals --- -With modals you can create pop-up forms that allow users to provide you with formatted inputs through submissions. We'll cover how to create, show, and receive modal forms using discord.js! +Modals are pop-up forms that allow you to prompt users for additional input. This form-like interaction response blocks the user from interacting with Discord until the modal is submitted or dismissed. In this section, we will cover how to create, show, and receive modals using discord.js! - This page is a follow-up to the [interactions (slash commands) page](../slash-commands/advanced-creation). Please - carefully read that section first, so that you can understand the methods used in this section. + This page is a follow-up to the [interactions (slash commands) page](../slash-commands/advanced-creation). Reading + that page first will help you understand the concepts introduced in this page. ## Building and responding with modals -Unlike message components, modals aren't strictly components themselves. They're a callback structure used to respond to interactions. +With the `ModalBuilder` class, discord.js offers a convenient way to build modals step by step using setters and callbacks. - You can have a maximum of five `ActionRowBuilder`s per modal builder, and one `TextInputBuilder` within an - `ActionRowBuilder`. Currently, you can only use `TextInputBuilder`s in modal action rows builders. + You can have a maximum of five top-level components per modal, each of which can be a label or a text display + component. -To create a modal you construct a new `ModalBuilder`. You can then use the setters to add the custom id and title. - ```js const { Events, ModalBuilder } = require('discord.js'); client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; - if (interaction.commandName === 'ping') { const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal'); @@ -35,105 +32,321 @@ client.on(Events.InteractionCreate, async (interaction) => { ``` - The custom id is a developer-defined string of up to 100 characters. Use this field to ensure you can uniquely define - all incoming interactions from your modals! + The `customId` is a developer-defined string of up to 100 characters and uniquely identifies this modal instance. You + can use it to differentiate incoming interactions. -The next step is to add the input fields in which users responding can enter free-text. Adding inputs is similar to adding components to messages. +The next step is adding components to the modal, which may either request input or present information. -At the end, we then call `ChatInputCommandInteraction#showModal` to display the modal to the user. +### Label - -If you're using typescript you'll need to specify the type of components your action row holds. This can be done by specifying the generic parameter in `ActionRowBuilder`: +Label components wrap around other modal components (text input, select menus, etc.) to add a label and description to it. +Since labels are not stand-alone components, we will use this example label to wrap a text input component in the next section: + +```js +const { LabelBuilder, ModalBuilder } = require('discord.js'); -```diff -- new ActionRowBuilder() -+ new ActionRowBuilder() +client.on(Events.InteractionCreate, async (interaction) => { + if (!interaction.isChatInputCommand()) return; + if (interaction.commandName === 'ping') { + // [!code focus:11] + // Create the modal + const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal'); + + // [!code ++:5] + const hobbiesLabel = new LabelBuilder() + // The label is a large header text that identifies the interactive component for the user. + .setLabel('What are some of your favorite hobbies?') + // The description is an additional optional subtext that aids the label. + .setDescription('Activities you like to participate in'); + + // [!code ++:2] + // Add label to the modal + modal.addLabelComponents(hobbiesLabel); + } +}); ``` + + The `label` field has a max length of 45 characters. The `description` field has a max length of 100 characters. +### Text input + +Text input components prompt users for single or multi line free-form text. + ```js -const { ActionRowBuilder, Events, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js'); +const { LabelBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js'); client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; - if (interaction.commandName === 'ping') { + // [!code focus:10] // Create the modal const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal'); - // Add components to modal - - // Create the text input components - const favoriteColorInput = new TextInputBuilder() - .setCustomId('favoriteColorInput') - // The label is the prompt the user sees for this input - .setLabel("What's your favorite color?") - // Short means only a single line of text - .setStyle(TextInputStyle.Short); - + // [!code ++:6] const hobbiesInput = new TextInputBuilder() .setCustomId('hobbiesInput') + // Short means a single line of text. + .setStyle(TextInputStyle.Short) + // Placeholder text displayed inside the text input box + .setPlaceholder('card games, films, books, etc.'); + + // [!code focus:10] + const hobbiesLabel = new LabelBuilder() + // The label is a large header that identifies the interactive component for the user. .setLabel("What's some of your favorite hobbies?") - // Paragraph means multiple lines of text. - .setStyle(TextInputStyle.Paragraph); + // The description is an additional optional subtext that aids the label. + .setDescription('Activities you like to participate in') + // [!code ++:2] + // Set text input as the component of the label + .setTextInputComponent(hobbiesInput); + + // Add the label to the modal + modal.addLabelComponents(hobbiesLabel); + } +}); +``` + +#### Input styles + +Discord offers two different input styles: + +- `Short`, a single-line text entry +- `Paragraph`, a multi-line text entry + +#### Input properties + +A text input field can be customized in a number of ways to apply validation or set default values via the following `TextInputBuilder` methods: + +```js +const input = new TextInputBuilder() + // Set the component id (this is not the custom id) + .setId(1) + // Set the maximum number of characters allowed + .setMaxLength(1_000) + // Set the minimum number of characters required for submission + .setMinLength(10) + // Set a default value to prefill the text input + .setValue('Default') + // Require a value in this text input field (defaults to true) + .setRequired(true); +``` - // An action row only holds one text input, - // so you need one action row per text input. - const firstActionRow = new ActionRowBuilder().addComponents(favoriteColorInput); - const secondActionRow = new ActionRowBuilder().addComponents(hobbiesInput); + + The `id` field is used to differentiate components within interactions (which text input, selection, etc.). In + contrast, the `customId` covered earlier identifies the interaction (which modal, command, etc.). + + +### Select menu + +Select menus allow you to limit user input to a preselected list of values. Discord also offers select menus linked directly to native Discord entities like users, roles, and channels. +Since they behave very similarly to how they do in messages, please refer to the [corresponding guide page](../interactive-components/select-menus) for more information on configuring select menus. + +Here again, you wrap the select menu with a label component to add context to the selection and add the label to the modal: - // Add inputs to the modal - modal.addComponents(firstActionRow, secondActionRow); +```js +// ... + +client.on(Events.InteractionCreate, async (interaction) => { + if (!interaction.isChatInputCommand()) return; + if (interaction.commandName === 'ping') { + // Create the modal + const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal'); - // Show the modal to the user - await interaction.showModal(modal); // [!code word:showModal] + // ... + + // [!code focus:24] + // [!code ++:23] + const favoriteStarterSelect = new StringSelectMenuBuilder() + .setCustomId('starter') + .setPlaceholder('Make a selection!') + // Modal only property on select menus to prevent submission, defaults to true + .setRequired(true) + .addOptions( + // String select menu options + new StringSelectMenuOptionBuilder() + // Label displayed to user + .setLabel('Bulbasaur') + // Description of option + .setDescription('The dual-type Grass/Poison Seed Pokémon.') + // Value returned to you in modal submission + .setValue('bulbasaur'), + new StringSelectMenuOptionBuilder() + .setLabel('Charmander') + .setDescription('The Fire-type Lizard Pokémon.') + .setValue('charmander'), + new StringSelectMenuOptionBuilder() + .setLabel('Squirtle') + .setDescription('The Water-type Tiny Turtle Pokémon.') + .setValue('squirtle'), + ); + + // ... + + // [!code focus:4] + // [!code ++:4] + const favoriteStarterLabel = new LabelBuilder() + .setLabel("What's your favorite Gen 1 Pokémon starter?") + // Set string select menu as component of the label + .setStringSelectMenuComponent(favoriteStarterSelect); + + // [!code focus:3] + // Add labels to modal + modal.addLabelComponents(hobbiesLabel); // [!code --] + modal.addLabelComponents(hobbiesLabel, favoriteStarterLabel); // [!code ++] } }); ``` -Restart your bot and invoke the `/ping` command again. You should see a popup form resembling the image below: +### Text display -![Modal Example](./images/modal-example.png) +Text display components offer you a way to give additional context to the user that doesn't fit into labels or isn't directly connected to any specific input field. + +```js +// ... + +client.on(Events.InteractionCreate, async (interaction) => { + if (!interaction.isChatInputCommand()) return; + if (interaction.commandName === 'ping') { + // Create the modal + const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal'); + + // ... + + // [!code focus:3] + // [!code ++:3] + const text = new TextDisplayBuilder().setContent( + 'Text that could not fit in to a label or description\n-# Markdown can also be used', + ); + + // [!code focus:5] + // Add components to modal + modal + // [!code --] + .addLabelComponents(hobbiesLabel, favoriteStarterLabel); + // [!code ++:2] + .addLabelComponents(hobbiesLabel, favoriteStarterLabel) + .addTextDisplayComponents(text); + } +}); +``` + +### File upload + +File upload components allow you to prompt the user to upload a file from their system. - Showing a modal must be the first response to an interaction. You cannot `defer()` or `deferUpdate()` then show a - modal later. + Discord **does not send the file data** itself in the resulting interaction. You will have to download it from + Discords CDN to process and validate it. Do not execute arbitrary code people upload via your app! -### Input styles +```js +// ... + +client.on(Events.InteractionCreate, async (interaction) => { + if (!interaction.isChatInputCommand()) return; + if (interaction.commandName === 'ping') { + // Create the modal + const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal'); + + // ... -Currently there are two different input styles available: + // [!code focus:2] + // [!code ++] + const pictureOfTheWeekUpload = new FileUploadBuilder().setCustomId('picture'); -- `Short`, a single-line text entry; -- `Paragraph`, a multi-line text entry similar to the HTML `