Compare commits

..

2 Commits

Author SHA1 Message Date
DeSqBlocki dafa81e2a6 added proper modal handler and raidplan 2026-08-14 23:49:31 +02:00
DeSqBlocki 9e94e99874 added more static files 2026-08-14 23:49:01 +02:00
15 changed files with 1696 additions and 44 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

+300
View File
@@ -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
View File
@@ -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
});
}
}
+60
View File
@@ -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`
);
};
+1
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff