added proper modal handler and raidplan
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
const {
|
||||
SlashCommandBuilder,
|
||||
ModalBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle,
|
||||
ActionRowBuilder
|
||||
} = require("discord.js");
|
||||
|
||||
const { ObjectId } = require("mongodb");
|
||||
const { mClient } = require("../..");
|
||||
|
||||
|
||||
const DATABASE = "neo-db";
|
||||
const DUTY_COLLECTION = "items_duties";
|
||||
|
||||
|
||||
const RAID_TYPES = [
|
||||
"Trial",
|
||||
"Raid",
|
||||
"AllianceRaid"
|
||||
];
|
||||
|
||||
const RAID_DIFFICULTIES = [
|
||||
"Extreme",
|
||||
"Unreal",
|
||||
"Savage",
|
||||
"Ultimate",
|
||||
"Chaotic"
|
||||
];
|
||||
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName("raid")
|
||||
.setDescription("Raid planning commands.")
|
||||
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName("plan")
|
||||
.setDescription("Create a new raid event.")
|
||||
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName("duty")
|
||||
.setDescription("Select the duty you want to run.")
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
),
|
||||
|
||||
|
||||
async execute(interaction) {
|
||||
switch (interaction.options.getSubcommand()) {
|
||||
case "plan":
|
||||
return planRaid(interaction);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async autocomplete(interaction) {
|
||||
return autocompleteDuty(interaction);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function autocompleteDuty(interaction) {
|
||||
const focused =
|
||||
interaction.options.getFocused();
|
||||
|
||||
const collection = mClient
|
||||
.db(DATABASE)
|
||||
.collection(DUTY_COLLECTION);
|
||||
|
||||
|
||||
const query = {
|
||||
Type: {
|
||||
$in: RAID_TYPES
|
||||
},
|
||||
|
||||
Difficulty: {
|
||||
$in: RAID_DIFFICULTIES
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (focused) {
|
||||
query.Name = {
|
||||
$regex: escapeRegex(focused),
|
||||
$options: "i"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const duties = await collection
|
||||
.find(query)
|
||||
.sort({
|
||||
Name: 1
|
||||
})
|
||||
.limit(25)
|
||||
.toArray();
|
||||
|
||||
|
||||
return interaction.respond(
|
||||
duties.map(duty => ({
|
||||
name: buildAutocompleteName(duty),
|
||||
value: duty._id.toString()
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
async function planRaid(interaction) {
|
||||
const dutyId =
|
||||
interaction.options.getString(
|
||||
"duty",
|
||||
true
|
||||
);
|
||||
|
||||
|
||||
if (!ObjectId.isValid(dutyId)) {
|
||||
return interaction.reply({
|
||||
content:
|
||||
"Please select a duty from the autocomplete results.",
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const collection = mClient
|
||||
.db(DATABASE)
|
||||
.collection(DUTY_COLLECTION);
|
||||
|
||||
|
||||
const duty = await collection.findOne({
|
||||
_id: new ObjectId(dutyId),
|
||||
|
||||
Type: {
|
||||
$in: RAID_TYPES
|
||||
},
|
||||
|
||||
Difficulty: {
|
||||
$in: RAID_DIFFICULTIES
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!duty) {
|
||||
return interaction.reply({
|
||||
content:
|
||||
"That duty could not be found or is not eligible for raid planning.",
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* The duty ObjectId is embedded in the modal customId.
|
||||
*
|
||||
* The modal handler can then retrieve the selected duty
|
||||
* without trusting editable modal input.
|
||||
*/
|
||||
const modal =
|
||||
buildRaidModal(duty);
|
||||
|
||||
|
||||
return interaction.showModal(modal);
|
||||
}
|
||||
|
||||
|
||||
function buildRaidModal(duty) {
|
||||
const modal =
|
||||
new ModalBuilder()
|
||||
.setCustomId(
|
||||
`raidPlan:${duty._id}`
|
||||
)
|
||||
.setTitle(
|
||||
"Shaiwase Raid Planner"
|
||||
);
|
||||
|
||||
|
||||
const nameInput =
|
||||
new TextInputBuilder()
|
||||
.setCustomId("raid-name")
|
||||
.setLabel("Name")
|
||||
.setPlaceholder(
|
||||
"Friday Savage Prog"
|
||||
)
|
||||
.setStyle(
|
||||
TextInputStyle.Short
|
||||
)
|
||||
.setMinLength(1)
|
||||
.setMaxLength(100)
|
||||
.setRequired(true);
|
||||
|
||||
|
||||
const descriptionInput =
|
||||
new TextInputBuilder()
|
||||
.setCustomId(
|
||||
"raid-description"
|
||||
)
|
||||
.setLabel("Description")
|
||||
.setPlaceholder(
|
||||
"What are we doing? Any requirements?"
|
||||
)
|
||||
.setStyle(
|
||||
TextInputStyle.Paragraph
|
||||
)
|
||||
.setMaxLength(1000)
|
||||
.setRequired(true);
|
||||
|
||||
|
||||
/*
|
||||
* Display only.
|
||||
*
|
||||
* We don't use this field to identify the duty.
|
||||
* The ObjectId in the modal customId is authoritative.
|
||||
*/
|
||||
const dutyInput =
|
||||
new TextInputBuilder()
|
||||
.setCustomId("raid-duty")
|
||||
.setLabel("Duty")
|
||||
.setValue(duty.Name)
|
||||
.setStyle(
|
||||
TextInputStyle.Short
|
||||
)
|
||||
.setRequired(true);
|
||||
|
||||
|
||||
const dateInput =
|
||||
new TextInputBuilder()
|
||||
.setCustomId("raid-date")
|
||||
.setLabel("Date")
|
||||
.setPlaceholder("DD-MM-YYYY")
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setMinLength(10)
|
||||
.setMaxLength(10)
|
||||
.setRequired(true);
|
||||
|
||||
|
||||
const timeInput =
|
||||
new TextInputBuilder()
|
||||
.setCustomId("raid-time")
|
||||
.setLabel("Start - End (24h clock)")
|
||||
.setPlaceholder("20:00 - 23:00")
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setMinLength(13)
|
||||
.setMaxLength(13)
|
||||
.setRequired(true);
|
||||
|
||||
|
||||
modal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(nameInput),
|
||||
|
||||
new ActionRowBuilder()
|
||||
.addComponents(descriptionInput),
|
||||
|
||||
new ActionRowBuilder()
|
||||
.addComponents(dutyInput),
|
||||
|
||||
new ActionRowBuilder()
|
||||
.addComponents(dateInput),
|
||||
|
||||
new ActionRowBuilder()
|
||||
.addComponents(timeInput)
|
||||
);
|
||||
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
|
||||
function buildAutocompleteName(duty) {
|
||||
const label =
|
||||
`${duty.Name} — ${formatDutyType(duty.Type)} / ${duty.Difficulty}`;
|
||||
|
||||
return label.slice(
|
||||
0,
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function formatDutyType(type) {
|
||||
switch (type) {
|
||||
case "AllianceRaid":
|
||||
return "Alliance Raid";
|
||||
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function escapeRegex(value) {
|
||||
return value.replace(
|
||||
/[.*+?^${}()|[\]\\]/g,
|
||||
"\\$&"
|
||||
);
|
||||
}
|
||||
+214
-44
@@ -1,97 +1,267 @@
|
||||
const { Events, MessageFlags, Message } = require('discord.js')
|
||||
const { mClient } = require('../index')
|
||||
require('dotenv').config()
|
||||
const {
|
||||
Events,
|
||||
MessageFlags
|
||||
} = require("discord.js");
|
||||
|
||||
require("dotenv").config();
|
||||
|
||||
|
||||
module.exports = {
|
||||
name: Events.InteractionCreate,
|
||||
once: false,
|
||||
async execute(interaction) {
|
||||
if (interaction.isChatInputCommand()) { commandHandler(interaction) }
|
||||
if (interaction.isButton()) { buttonHandler(interaction) }
|
||||
if (interaction.isMentionableSelectMenu()) { selectMenuHandler(interaction) }
|
||||
if (interaction.isUserSelectMenu()) { selectMenuHandler(interaction) }
|
||||
if (interaction.isModalSubmit()) { modalHandler(interaction) }
|
||||
}
|
||||
}
|
||||
async function commandHandler(interaction) {
|
||||
const command = interaction.client.commands.get(interaction.commandName);
|
||||
|
||||
if (!command) { return console.error(`No command matching ${interaction.commandName} was found.`) }
|
||||
async execute(interaction) {
|
||||
if (interaction.isAutocomplete()) {
|
||||
return autocompleteHandler(interaction);
|
||||
}
|
||||
|
||||
if (interaction.isChatInputCommand()) {
|
||||
return commandHandler(interaction);
|
||||
}
|
||||
|
||||
if (interaction.isButton()) {
|
||||
return buttonHandler(interaction);
|
||||
}
|
||||
|
||||
if (
|
||||
interaction.isMentionableSelectMenu() ||
|
||||
interaction.isUserSelectMenu()
|
||||
) {
|
||||
return selectMenuHandler(interaction);
|
||||
}
|
||||
|
||||
if (interaction.isModalSubmit()) {
|
||||
return modalHandler(interaction);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function autocompleteHandler(interaction) {
|
||||
const command =
|
||||
interaction.client.commands.get(
|
||||
interaction.commandName
|
||||
);
|
||||
|
||||
|
||||
if (!command) {
|
||||
return console.error(
|
||||
`No command matching ${interaction.commandName} was found.`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
typeof command.autocomplete !==
|
||||
"function"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await command.execute(interaction);
|
||||
await command.autocomplete(
|
||||
interaction
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Autocomplete error for /${interaction.commandName}:`,
|
||||
error
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Autocomplete cannot use normal interaction.reply().
|
||||
* If something failed, send an empty set so Discord
|
||||
* stops waiting.
|
||||
*/
|
||||
try {
|
||||
await interaction.respond([]);
|
||||
} catch {
|
||||
// Interaction may already be acknowledged or expired.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function commandHandler(interaction) {
|
||||
const command =
|
||||
interaction.client.commands.get(
|
||||
interaction.commandName
|
||||
);
|
||||
|
||||
|
||||
if (!command) {
|
||||
return console.error(
|
||||
`No command matching ${interaction.commandName} was found.`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await command.execute(
|
||||
interaction
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
|
||||
if (
|
||||
interaction.replied ||
|
||||
interaction.deferred
|
||||
) {
|
||||
await interaction.followUp({
|
||||
content: 'There was an error while executing this command!',
|
||||
flags: MessageFlags.Ephemeral
|
||||
content:
|
||||
"There was an error while executing this command!",
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
} else {
|
||||
await interaction.reply({
|
||||
content: 'There was an error while executing this command!',
|
||||
flags: MessageFlags.Ephemeral
|
||||
content:
|
||||
"There was an error while executing this command!",
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function buttonHandler(interaction) {
|
||||
const button = interaction.client.buttons.get(interaction.customId)
|
||||
if (!button) { return console.error(`No button registered matching ${interaction.customId}.`) }
|
||||
const button =
|
||||
interaction.client.buttons.get(
|
||||
interaction.customId
|
||||
);
|
||||
|
||||
|
||||
if (!button) {
|
||||
return console.error(
|
||||
`No button registered matching ${interaction.customId}.`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await button.execute(interaction);
|
||||
await button.execute(
|
||||
interaction
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
|
||||
if (
|
||||
interaction.replied ||
|
||||
interaction.deferred
|
||||
) {
|
||||
await interaction.followUp({
|
||||
content: 'There was an error while executing this command!',
|
||||
flags: MessageFlags.Ephemeral
|
||||
content:
|
||||
"There was an error while executing this button!",
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
} else {
|
||||
await interaction.reply({
|
||||
content: 'There was an error while executing this command!',
|
||||
flags: MessageFlags.Ephemeral
|
||||
content:
|
||||
"There was an error while executing this button!",
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function selectMenuHandler(interaction) {
|
||||
const selectMenu = interaction.client.selectMenus.get(interaction.customId)
|
||||
if (!selectMenu) { return console.error(`No Select Menu registered matching ${interaction.customId}.`) }
|
||||
const selectMenu =
|
||||
interaction.client.selectMenus.get(
|
||||
interaction.customId
|
||||
);
|
||||
|
||||
|
||||
if (!selectMenu) {
|
||||
return console.error(
|
||||
`No Select Menu registered matching ${interaction.customId}.`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await selectMenu.execute(interaction);
|
||||
await selectMenu.execute(
|
||||
interaction
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
|
||||
if (
|
||||
interaction.replied ||
|
||||
interaction.deferred
|
||||
) {
|
||||
await interaction.followUp({
|
||||
content: 'There was an error while executing this command!',
|
||||
flags: MessageFlags.Ephemeral
|
||||
content:
|
||||
"There was an error while executing this select menu!",
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
} else {
|
||||
await interaction.reply({
|
||||
content: 'There was an error while executing this command!',
|
||||
flags: MessageFlags.Ephemeral
|
||||
content:
|
||||
"There was an error while executing this select menu!",
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function modalHandler(interaction) {
|
||||
const modal = interaction.client.modals.get(interaction.customId);
|
||||
if (!modal) { return console.error(`No Modal registered matching ${interaction.customId}.`); }
|
||||
const baseCustomId =
|
||||
interaction.customId.split(":")[0];
|
||||
|
||||
const modal =
|
||||
interaction.client.modals.get(
|
||||
baseCustomId
|
||||
);
|
||||
|
||||
|
||||
if (!modal) {
|
||||
return console.error(
|
||||
`No Modal registered matching ${interaction.customId}.`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await modal.execute(interaction);
|
||||
await modal.execute(
|
||||
interaction
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
if (
|
||||
interaction.replied ||
|
||||
interaction.deferred
|
||||
) {
|
||||
await interaction.followUp({
|
||||
content: 'There was an error while executing this modal!',
|
||||
flags: MessageFlags.Ephemeral
|
||||
content:
|
||||
"There was an error while executing this modal!",
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
} else {
|
||||
await interaction.reply({
|
||||
content: 'There was an error while executing this modal!',
|
||||
flags: MessageFlags.Ephemeral
|
||||
content:
|
||||
"There was an error while executing this modal!",
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
module.exports = (client) => {
|
||||
const modalsPath = path.join(
|
||||
__dirname,
|
||||
"../modals"
|
||||
);
|
||||
|
||||
if (!fs.existsSync(modalsPath)) {
|
||||
console.log(
|
||||
`[WARNING] Modal directory not found: ${modalsPath}`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const modalFiles = fs
|
||||
.readdirSync(modalsPath)
|
||||
.filter(file =>
|
||||
file.endsWith(".js")
|
||||
);
|
||||
|
||||
let count = 0;
|
||||
|
||||
for (const file of modalFiles) {
|
||||
const filePath = path.join(
|
||||
modalsPath,
|
||||
file
|
||||
);
|
||||
|
||||
const modal = require(filePath);
|
||||
|
||||
if (
|
||||
!modal.customId ||
|
||||
typeof modal.execute !== "function"
|
||||
) {
|
||||
console.log(
|
||||
`[WARNING] Modal ${file} is missing "customId" or "execute".`
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
client.modals.set(
|
||||
modal.customId,
|
||||
modal
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[MODAL] Registered "${modal.customId}" from ${file}`
|
||||
);
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[${count}] Modal(s) initialized`
|
||||
);
|
||||
};
|
||||
@@ -25,6 +25,7 @@ client.legacyCommands = new Collection() // legacy commands collection
|
||||
client.aliases = new Collection() // list of command aliases
|
||||
client.buttons = new Collection() // list of buttons
|
||||
client.selectMenus = new Collection() // list of selectMenus
|
||||
client.modals = new Collection();
|
||||
|
||||
fs.readdirSync('./handlers').forEach((handler) => {
|
||||
require(`./handlers/${handler}`)(client)
|
||||
|
||||
+1121
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user