インタラクション(Button / Select / Modal)
メッセージにボタン・セレクトメニューを貼って、Modal(ダイアログ)で入力させる。
ユーザー操作のたびに InteractionCreate が飛んでくる。
ActionRow に並べる
ボタンや SelectMenu は ActionRow(最大 5 つ)に入れて送る。1 メッセージで最大 5 行。
Button
import {
ActionRowBuilder, ButtonBuilder, ButtonStyle,
} from "discord.js"
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId("confirm")
.setLabel("OK")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId("cancel")
.setLabel("キャンセル")
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setLabel("詳細")
.setURL("https://example.com")
.setStyle(ButtonStyle.Link), // URL のみ
new ButtonBuilder()
.setCustomId("delete")
.setLabel("削除")
.setEmoji("🗑️")
.setStyle(ButtonStyle.Danger),
)
await interaction.reply({
content: "実行しますか?",
components: [row],
})
ボタンスタイル
Primary(青) — 主要アクションSecondary(グレー) — 副次Success(緑) — 成功・完了Danger(赤) — 破壊的操作Link(外部リンク) — URL を開く
ボタンの押下を受ける
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isButton()) return
if (interaction.customId === "confirm") {
await interaction.update({ content: "OKを受け付けました", components: [] })
}
if (interaction.customId === "cancel") {
await interaction.update({ content: "キャンセルしました", components: [] })
}
})
interaction.update(...)— 元のメッセージを書き換えinteraction.reply(...)— 新しいメッセージで返すinteraction.deferUpdate()— まず ACK して後で update
String Select Menu
import {
ActionRowBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder,
} from "discord.js"
const select = new StringSelectMenuBuilder()
.setCustomId("color")
.setPlaceholder("色を選んで")
.setMinValues(1)
.setMaxValues(2) // 複数選択可能
.addOptions(
new StringSelectMenuOptionBuilder()
.setLabel("赤").setValue("red").setEmoji("🟥"),
new StringSelectMenuOptionBuilder()
.setLabel("青").setValue("blue").setEmoji("🟦"),
)
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(select)
await interaction.reply({ content: "選択", components: [row] })
// ハンドラ
if (interaction.isStringSelectMenu() && interaction.customId === "color") {
const values = interaction.values // string[]
await interaction.update({ content: `選択: ${values.join(", ")}` })
}
その他の SelectMenu 種別
UserSelectMenuBuilder— ユーザー選択RoleSelectMenuBuilder— ロール選択ChannelSelectMenuBuilder— チャンネル選択MentionableSelectMenuBuilder— メンション可能エンティティ
Modal(ダイアログで入力)
import {
ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder,
} from "discord.js"
// ボタン押下時に Modal を出す
if (interaction.isButton() && interaction.customId === "open_form") {
const modal = new ModalBuilder()
.setCustomId("feedback")
.setTitle("フィードバック")
const titleInput = new TextInputBuilder()
.setCustomId("title")
.setLabel("タイトル")
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setMaxLength(100)
const bodyInput = new TextInputBuilder()
.setCustomId("body")
.setLabel("内容")
.setStyle(TextInputStyle.Paragraph)
.setMinLength(10)
.setMaxLength(2000)
modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(titleInput),
new ActionRowBuilder<TextInputBuilder>().addComponents(bodyInput),
)
await interaction.showModal(modal)
}
// Modal 送信を受ける
if (interaction.isModalSubmit() && interaction.customId === "feedback") {
const title = interaction.fields.getTextInputValue("title")
const body = interaction.fields.getTextInputValue("body")
await interaction.reply({ content: `受付: ${title}`, ephemeral: true })
}
TextInputStyle
Short— 1行入力Paragraph— 複数行
customId に状態を埋め込む
ボタンの customId にJSON や ID を埋め込んで後でパースする定石。
// 送信時
new ButtonBuilder().setCustomId(`vote:${pollId}:yes`)
// 受信時
const [kind, pollId, value] = interaction.customId.split(":")
if (kind === "vote") {
await recordVote(pollId, value, interaction.user.id)
}
collector パターン(特定メッセージの応答だけ拾う)
const message = await interaction.reply({
content: "実行しますか?",
components: [row],
fetchReply: true,
})
const collector = message.createMessageComponentCollector({
filter: (i) => i.user.id === interaction.user.id, // 本人だけ
time: 60_000, // 60秒
max: 1,
})
collector.on("collect", async (i) => {
await i.update({ content: `押した: ${i.customId}`, components: [] })
})
collector.on("end", async (collected, reason) => {
if (collected.size === 0) {
await interaction.editReply({ content: "タイムアウト", components: [] })
}
})
カスタム入力の検証
Modal や Select の値は必ずサーバー側で検証する(Discord 経由でも改ざんされうるという前提で)。
Components V2(新しい構文)
Discord はComponents V2(より柔軟なレイアウト)を導入中。
通常の ActionRow ベースで十分だが、複雑な UI には V2 を検討する余地がある。
詳しくは 公式 docs。
注意点
- 1 メッセージに ActionRow は5つまで、Button は1 row 5つまで
- SelectMenu は1 row につき1個
- Modal は5 行まで、1行に1つの TextInput
- Button → Modal の連鎖は OK だが、Modal → Modal は不可
- ephemeral message に Modal を出すならModal を返してはいけない(reply するなら ephemeral=true で)