657 lines
22 KiB
JavaScript
657 lines
22 KiB
JavaScript
/**
|
||
* /setup wizard – multi-step panel configuration (panel name, support role,
|
||
* ticket category, transcript channel, panel channel).
|
||
*/
|
||
const {
|
||
ActionRowBuilder,
|
||
ButtonBuilder,
|
||
ButtonStyle,
|
||
EmbedBuilder,
|
||
ChannelType,
|
||
ModalBuilder,
|
||
TextInputBuilder,
|
||
TextInputStyle,
|
||
RoleSelectMenuBuilder,
|
||
ChannelSelectMenuBuilder
|
||
} = require('discord.js');
|
||
const { CONFIG } = require('../config');
|
||
const { enqueueSend } = require('../services/channelQueue');
|
||
|
||
const TOTAL_STEPS = 5;
|
||
const WIZARD_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
||
|
||
/** @type {Map<string, { step: number, panelName?: string, roleIds?: string[], ticketType?: 'channel'|'thread', categoryId?: string, categoryName?: string, threadChannelId?: string, threadChannelName?: string, transcriptChannelId?: string, panelChannelId?: string, createdAt: number }>} */
|
||
const setupState = new Map();
|
||
|
||
const PREFIX = 'setup_';
|
||
const PREFIX_BUTTON = PREFIX;
|
||
const PREFIX_MODAL = PREFIX + 'modal_';
|
||
const PREFIX_SELECT = PREFIX + 'select_';
|
||
|
||
function getState(userId) {
|
||
const s = setupState.get(userId);
|
||
if (!s) return null;
|
||
if (Date.now() - s.createdAt > WIZARD_TIMEOUT_MS) {
|
||
setupState.delete(userId);
|
||
return null;
|
||
}
|
||
return s;
|
||
}
|
||
|
||
function setState(userId, data) {
|
||
const existing = setupState.get(userId) || { createdAt: Date.now() };
|
||
setupState.set(userId, { ...existing, ...data });
|
||
}
|
||
|
||
function clearState(userId) {
|
||
setupState.delete(userId);
|
||
}
|
||
|
||
function step1Embed(panelName) {
|
||
const embed = new EmbedBuilder()
|
||
.setColor(0x2ecc71)
|
||
.setTitle('Step 1/5 Set the panel name')
|
||
.setDescription(
|
||
'Use the button to set the panel name and continue.\n(This can be changed later.)'
|
||
)
|
||
.addFields({ name: 'Current Name', value: panelName ? `\`${panelName}\`` : 'Not set' });
|
||
|
||
const row = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'setname')
|
||
.setLabel('Set name')
|
||
.setStyle(ButtonStyle.Primary)
|
||
.setEmoji('⚙️'),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'continue_1')
|
||
.setLabel('Save & Continue')
|
||
.setStyle(ButtonStyle.Success)
|
||
.setDisabled(!panelName)
|
||
);
|
||
return { embeds: [embed], components: [row] };
|
||
}
|
||
|
||
function step2Embed(roleLabels) {
|
||
const embed = new EmbedBuilder()
|
||
.setColor(0x2ecc71)
|
||
.setTitle('Step 2/5 Select the support team role(s)')
|
||
.setDescription(
|
||
'The support roles will be automatically added to this panel\'s tickets so they can assist people as needed.\n' +
|
||
'Use the dropdown to select roles.\n' +
|
||
'Not seeing your role? Try searching for it inside the dropdown.'
|
||
)
|
||
.addFields({
|
||
name: 'Selected Role(s)',
|
||
value: roleLabels && roleLabels.length ? roleLabels.join(', ') : 'None selected'
|
||
});
|
||
|
||
const select = new RoleSelectMenuBuilder()
|
||
.setCustomId(PREFIX_SELECT + 'roles')
|
||
.setPlaceholder('Select all the roles for your support team')
|
||
.setMinValues(1)
|
||
.setMaxValues(5);
|
||
|
||
const row1 = new ActionRowBuilder().addComponents(select);
|
||
const row2 = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'back_2')
|
||
.setLabel('Back')
|
||
.setStyle(ButtonStyle.Secondary)
|
||
.setEmoji('⬅️'),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'continue_2')
|
||
.setLabel('Save & Continue')
|
||
.setStyle(ButtonStyle.Success)
|
||
.setDisabled(!roleLabels || roleLabels.length === 0)
|
||
);
|
||
return { embeds: [embed], components: [row1, row2] };
|
||
}
|
||
|
||
function step3Embed(state) {
|
||
const ticketType = state.ticketType;
|
||
const categoryName = state.categoryName;
|
||
const threadChannelName = state.threadChannelName;
|
||
|
||
if (!ticketType) {
|
||
const embed = new EmbedBuilder()
|
||
.setColor(0x2ecc71)
|
||
.setTitle('Step 3/5 How should tickets be created?')
|
||
.setDescription(
|
||
'**Channels:** Each ticket is a channel in a category (classic layout).\n' +
|
||
'**Threads:** Each ticket is a private thread under a text channel (compact).\n' +
|
||
'**Both:** Create one panel with two buttons (thread + category).'
|
||
)
|
||
.addFields({ name: 'Choice', value: 'Select below' });
|
||
|
||
const row = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'tickettype_channel')
|
||
.setLabel('Channels in category')
|
||
.setStyle(ButtonStyle.Primary)
|
||
.setEmoji('📁'),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'tickettype_thread')
|
||
.setLabel('Private threads')
|
||
.setStyle(ButtonStyle.Primary)
|
||
.setEmoji('🧵'),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'tickettype_both')
|
||
.setLabel('Both (thread + category)')
|
||
.setStyle(ButtonStyle.Primary)
|
||
.setEmoji('📋'),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'back_3')
|
||
.setLabel('Back')
|
||
.setStyle(ButtonStyle.Secondary)
|
||
.setEmoji('⬅️')
|
||
);
|
||
return { embeds: [embed], components: [row] };
|
||
}
|
||
|
||
if (ticketType === 'both') {
|
||
const embed = new EmbedBuilder()
|
||
.setColor(0x2ecc71)
|
||
.setTitle('Step 3/5 Select category and thread channel (both)')
|
||
.setDescription(
|
||
'The panel will have two buttons: one creates ticket **threads**, one creates ticket **channels**.\n' +
|
||
'Select the category for channels and the text channel for threads.'
|
||
)
|
||
.addFields(
|
||
{ name: 'Category (for channels)', value: categoryName ? `\`${categoryName}\`` : 'None selected', inline: true },
|
||
{ name: 'Channel (for threads)', value: threadChannelName ? `\`${threadChannelName}\`` : 'None selected', inline: true }
|
||
);
|
||
|
||
const row1 = new ActionRowBuilder().addComponents(
|
||
new ChannelSelectMenuBuilder()
|
||
.setCustomId(PREFIX_SELECT + 'category')
|
||
.setPlaceholder('Select category for channels')
|
||
.addChannelTypes(ChannelType.GuildCategory)
|
||
.setMaxValues(1)
|
||
);
|
||
const row2 = new ActionRowBuilder().addComponents(
|
||
new ChannelSelectMenuBuilder()
|
||
.setCustomId(PREFIX_SELECT + 'thread_channel')
|
||
.setPlaceholder('Select channel for threads')
|
||
.addChannelTypes(ChannelType.GuildText)
|
||
.setMaxValues(1)
|
||
);
|
||
const row3 = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'tickettype_clear_both_channel')
|
||
.setLabel('Channels only')
|
||
.setStyle(ButtonStyle.Secondary),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'tickettype_clear_thread')
|
||
.setLabel('Threads only')
|
||
.setStyle(ButtonStyle.Secondary),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'back_3')
|
||
.setLabel('Back')
|
||
.setStyle(ButtonStyle.Secondary)
|
||
.setEmoji('⬅️'),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'continue_3')
|
||
.setLabel('Save & Continue')
|
||
.setStyle(ButtonStyle.Success)
|
||
.setDisabled(!(categoryName && threadChannelName))
|
||
);
|
||
return { embeds: [embed], components: [row1, row2, row3] };
|
||
}
|
||
|
||
if (ticketType === 'channel') {
|
||
const embed = new EmbedBuilder()
|
||
.setColor(0x2ecc71)
|
||
.setTitle('Step 3/5 Select the ticket category')
|
||
.setDescription(
|
||
'The selected category is where ticket **channels** will be created.\n' +
|
||
'Use the dropdown to select the category.'
|
||
)
|
||
.addFields({ name: 'Selected Category', value: categoryName ? `\`${categoryName}\`` : 'None selected' });
|
||
|
||
const select = new ChannelSelectMenuBuilder()
|
||
.setCustomId(PREFIX_SELECT + 'category')
|
||
.setPlaceholder('Select a category')
|
||
.addChannelTypes(ChannelType.GuildCategory)
|
||
.setMaxValues(1);
|
||
|
||
const row1 = new ActionRowBuilder().addComponents(select);
|
||
const row2 = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'tickettype_clear')
|
||
.setLabel('Change to Threads')
|
||
.setStyle(ButtonStyle.Secondary),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'back_3')
|
||
.setLabel('Back')
|
||
.setStyle(ButtonStyle.Secondary)
|
||
.setEmoji('⬅️'),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'continue_3')
|
||
.setLabel('Save & Continue')
|
||
.setStyle(ButtonStyle.Success)
|
||
.setDisabled(!categoryName)
|
||
);
|
||
return { embeds: [embed], components: [row1, row2] };
|
||
}
|
||
|
||
// ticketType === 'thread'
|
||
const embed = new EmbedBuilder()
|
||
.setColor(0x2ecc71)
|
||
.setTitle('Step 3/5 Select the channel for ticket threads')
|
||
.setDescription(
|
||
'Ticket **threads** will be created as private threads under the selected text channel.\n' +
|
||
'Use the dropdown to select the channel.'
|
||
)
|
||
.addFields({ name: 'Selected Channel', value: threadChannelName ? `\`${threadChannelName}\`` : 'None selected' });
|
||
|
||
const select = new ChannelSelectMenuBuilder()
|
||
.setCustomId(PREFIX_SELECT + 'thread_channel')
|
||
.setPlaceholder('Select a text channel')
|
||
.addChannelTypes(ChannelType.GuildText)
|
||
.setMaxValues(1);
|
||
|
||
const row1 = new ActionRowBuilder().addComponents(select);
|
||
const row2 = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'tickettype_clear')
|
||
.setLabel('Change to Channels')
|
||
.setStyle(ButtonStyle.Secondary),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'back_3')
|
||
.setLabel('Back')
|
||
.setStyle(ButtonStyle.Secondary)
|
||
.setEmoji('⬅️'),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'continue_3')
|
||
.setLabel('Save & Continue')
|
||
.setStyle(ButtonStyle.Success)
|
||
.setDisabled(!threadChannelName)
|
||
);
|
||
return { embeds: [embed], components: [row1, row2] };
|
||
}
|
||
|
||
function step4Embed(channelName) {
|
||
const embed = new EmbedBuilder()
|
||
.setColor(0x2ecc71)
|
||
.setTitle('Step 4/5 Select the transcript channel')
|
||
.setDescription(
|
||
'The selected channel is where transcripts will be saved when tickets are closed.\n' +
|
||
'Use the dropdown to select the channel.\n' +
|
||
'Not seeing your channel? Try searching for it inside the dropdown.'
|
||
)
|
||
.addFields({
|
||
name: 'Selected Channel',
|
||
value: channelName ? `\`${channelName}\`` : 'Not selected'
|
||
});
|
||
|
||
const select = new ChannelSelectMenuBuilder()
|
||
.setCustomId(PREFIX_SELECT + 'transcript')
|
||
.setPlaceholder('Select a channel')
|
||
.addChannelTypes(ChannelType.GuildText)
|
||
.setMaxValues(1);
|
||
|
||
const row1 = new ActionRowBuilder().addComponents(select);
|
||
const row2 = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'back_4')
|
||
.setLabel('Back')
|
||
.setStyle(ButtonStyle.Secondary)
|
||
.setEmoji('⬅️'),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'continue_4')
|
||
.setLabel('Save & Continue')
|
||
.setStyle(ButtonStyle.Success)
|
||
.setDisabled(!channelName)
|
||
);
|
||
return { embeds: [embed], components: [row1, row2] };
|
||
}
|
||
|
||
function step5Embed(channelName) {
|
||
const embed = new EmbedBuilder()
|
||
.setColor(0x2ecc71)
|
||
.setTitle('Step 5/5 Send the panel into a channel')
|
||
.setDescription(
|
||
'The ticket creation panel is what the community will use to create tickets.\n' +
|
||
'Use the dropdown to select the channel to send the panel into.\n' +
|
||
'Not seeing your channel? Try searching for it inside the dropdown.\n' +
|
||
'Sending not working? Run `/panel` in the channel directly.'
|
||
)
|
||
.addFields({
|
||
name: 'Selected Channel',
|
||
value: channelName ? `\`${channelName}\`` : 'Not selected'
|
||
});
|
||
|
||
const select = new ChannelSelectMenuBuilder()
|
||
.setCustomId(PREFIX_SELECT + 'panel_channel')
|
||
.setPlaceholder('Select a channel')
|
||
.addChannelTypes(ChannelType.GuildText)
|
||
.setMaxValues(1);
|
||
|
||
const row1 = new ActionRowBuilder().addComponents(select);
|
||
const row2 = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'back_5')
|
||
.setLabel('Back')
|
||
.setStyle(ButtonStyle.Secondary)
|
||
.setEmoji('⬅️'),
|
||
new ButtonBuilder()
|
||
.setCustomId(PREFIX_BUTTON + 'finish')
|
||
.setLabel('Finish')
|
||
.setStyle(ButtonStyle.Success)
|
||
.setDisabled(!channelName)
|
||
);
|
||
return { embeds: [embed], components: [row1, row2] };
|
||
}
|
||
|
||
/**
|
||
* Handle /setup slash command – send Step 1.
|
||
*/
|
||
async function handleSetupCommand(interaction) {
|
||
await interaction.deferReply({ ephemeral: true });
|
||
setState(interaction.user.id, { step: 1, panelName: null });
|
||
const payload = step1Embed(null);
|
||
await interaction.editReply(payload);
|
||
}
|
||
|
||
/**
|
||
* Handle setup button (Set name, Back, Save & Continue, Finish).
|
||
*/
|
||
async function handleSetupButton(interaction) {
|
||
const customId = interaction.customId;
|
||
if (!customId.startsWith(PREFIX_BUTTON)) return false;
|
||
|
||
const userId = interaction.user.id;
|
||
const state = getState(userId);
|
||
if (!state) {
|
||
await interaction.reply({
|
||
content: 'This setup session has expired. Run `/setup` again.',
|
||
ephemeral: true
|
||
}).catch(() => {});
|
||
return true;
|
||
}
|
||
|
||
// Set name → show modal
|
||
if (customId === PREFIX_BUTTON + 'setname') {
|
||
const modal = new ModalBuilder()
|
||
.setCustomId(PREFIX_MODAL + 'name')
|
||
.setTitle('Panel name');
|
||
|
||
const input = new TextInputBuilder()
|
||
.setCustomId('panel_name')
|
||
.setLabel('Panel name')
|
||
.setStyle(TextInputStyle.Short)
|
||
.setPlaceholder('e.g. New Panel')
|
||
.setRequired(true)
|
||
.setMaxLength(100);
|
||
if (state.panelName) input.setValue(state.panelName);
|
||
modal.addComponents(new ActionRowBuilder().addComponents(input));
|
||
await interaction.showModal(modal);
|
||
return true;
|
||
}
|
||
|
||
// Back
|
||
if (customId.startsWith(PREFIX_BUTTON + 'back_')) {
|
||
const step = parseInt(customId.replace(PREFIX_BUTTON + 'back_', ''), 10);
|
||
const nextStep = step - 1;
|
||
setState(userId, { step: nextStep });
|
||
let payload;
|
||
if (nextStep === 1) payload = step1Embed(state.panelName);
|
||
else if (nextStep === 2) payload = step2Embed(state.roleLabels);
|
||
else if (nextStep === 3) payload = step3Embed(state);
|
||
else if (nextStep === 4) payload = step4Embed(state.transcriptChannelName);
|
||
else payload = step5Embed(state.panelChannelName);
|
||
await interaction.update(payload);
|
||
return true;
|
||
}
|
||
|
||
// Save & Continue (steps 1–4)
|
||
if (customId === PREFIX_BUTTON + 'continue_1') {
|
||
setState(userId, { step: 2 });
|
||
await interaction.update(step2Embed(state.roleLabels));
|
||
return true;
|
||
}
|
||
if (customId === PREFIX_BUTTON + 'continue_2') {
|
||
setState(userId, { step: 3 });
|
||
await interaction.update(step3Embed({ ...state, step: 3 }));
|
||
return true;
|
||
}
|
||
if (customId === PREFIX_BUTTON + 'tickettype_channel') {
|
||
setState(userId, { ticketType: 'channel', categoryId: null, categoryName: null, threadChannelId: null, threadChannelName: null });
|
||
await interaction.update(step3Embed(getState(userId)));
|
||
return true;
|
||
}
|
||
if (customId === PREFIX_BUTTON + 'tickettype_thread') {
|
||
setState(userId, { ticketType: 'thread', categoryId: null, categoryName: null, threadChannelId: null, threadChannelName: null });
|
||
await interaction.update(step3Embed(getState(userId)));
|
||
return true;
|
||
}
|
||
if (customId === PREFIX_BUTTON + 'tickettype_both') {
|
||
setState(userId, { ticketType: 'both', categoryId: null, categoryName: null, threadChannelId: null, threadChannelName: null });
|
||
await interaction.update(step3Embed(getState(userId)));
|
||
return true;
|
||
}
|
||
if (customId === PREFIX_BUTTON + 'tickettype_clear') {
|
||
setState(userId, { ticketType: null, categoryId: null, categoryName: null, threadChannelId: null, threadChannelName: null });
|
||
await interaction.update(step3Embed(getState(userId)));
|
||
return true;
|
||
}
|
||
if (customId === PREFIX_BUTTON + 'tickettype_clear_thread') {
|
||
setState(userId, { ticketType: 'thread', categoryId: null, categoryName: null });
|
||
await interaction.update(step3Embed(getState(userId)));
|
||
return true;
|
||
}
|
||
if (customId === PREFIX_BUTTON + 'tickettype_clear_both_channel') {
|
||
setState(userId, { ticketType: 'channel', threadChannelId: null, threadChannelName: null });
|
||
await interaction.update(step3Embed(getState(userId)));
|
||
return true;
|
||
}
|
||
if (customId === PREFIX_BUTTON + 'continue_3') {
|
||
setState(userId, { step: 4 });
|
||
await interaction.update(step4Embed(state.transcriptChannelName));
|
||
return true;
|
||
}
|
||
if (customId === PREFIX_BUTTON + 'continue_4') {
|
||
setState(userId, { step: 5 });
|
||
await interaction.update(step5Embed(state.panelChannelName));
|
||
return true;
|
||
}
|
||
|
||
// Finish
|
||
if (customId === PREFIX_BUTTON + 'finish') {
|
||
const hasTicketTarget =
|
||
(state.ticketType === 'channel' && state.categoryId) ||
|
||
(state.ticketType === 'thread' && state.threadChannelId) ||
|
||
(state.ticketType === 'both' && state.categoryId && state.threadChannelId);
|
||
if (!state.panelChannelId || !hasTicketTarget || !state.roleIds?.length) {
|
||
await interaction.reply({
|
||
content: 'Please complete all steps (panel name, support role, ticket type + category/channel, transcript channel, panel channel).',
|
||
ephemeral: true
|
||
}).catch(() => {});
|
||
return true;
|
||
}
|
||
|
||
try {
|
||
const channel = await interaction.client.channels.fetch(state.panelChannelId);
|
||
const title = state.panelName || 'Indifferent Broccoli Tickets';
|
||
const description = 'Need help? Click below to create a ticket. 🎟';
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setTitle(title)
|
||
.setDescription(description)
|
||
.setColor(0x2ecc71)
|
||
.setThumbnail(CONFIG.LOGO_URL || null)
|
||
.setFooter({ text: 'Indifferent Broccoli Tickets' });
|
||
|
||
let row;
|
||
if (state.ticketType === 'both') {
|
||
row = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId('open_ticket_thread')
|
||
.setLabel('Create ticket (thread)')
|
||
.setStyle(ButtonStyle.Success)
|
||
.setEmoji('🧵'),
|
||
new ButtonBuilder()
|
||
.setCustomId('open_ticket_channel')
|
||
.setLabel('Create ticket (channel)')
|
||
.setStyle(ButtonStyle.Success)
|
||
.setEmoji('📁')
|
||
);
|
||
} else {
|
||
row = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId('open_ticket')
|
||
.setLabel('Create ticket')
|
||
.setStyle(ButtonStyle.Success)
|
||
.setEmoji('✅')
|
||
);
|
||
}
|
||
|
||
await enqueueSend(channel, { embeds: [embed], components: [row] });
|
||
|
||
const envLines = state.ticketType === 'both'
|
||
? [`DISCORD_THREAD_CHANNEL_ID=${state.threadChannelId}`, `DISCORD_TICKET_CATEGORY_ID=${state.categoryId}`]
|
||
: [state.ticketType === 'thread'
|
||
? `DISCORD_THREAD_CHANNEL_ID=${state.threadChannelId}`
|
||
: `DISCORD_TICKET_CATEGORY_ID=${state.categoryId}`];
|
||
const envSnippet = [
|
||
'**Add these to your `.env` file** (optional – only if you want to use these values for new Discord tickets):',
|
||
'```',
|
||
...envLines,
|
||
`ROLE_ID_TO_PING=${state.roleIds[0]}`,
|
||
`TRANSCRIPT_CHANNEL_ID=${state.transcriptChannelId}`,
|
||
`LOGGING_CHANNEL_ID=${state.transcriptChannelId}`,
|
||
'```'
|
||
].join('\n');
|
||
|
||
await interaction.update({
|
||
embeds: [
|
||
new EmbedBuilder()
|
||
.setColor(0x2ecc71)
|
||
.setTitle('Setup complete')
|
||
.setDescription(
|
||
`Panel **${title}** has been sent to ${channel}.\n\n` +
|
||
envSnippet
|
||
)
|
||
],
|
||
components: []
|
||
});
|
||
} catch (err) {
|
||
console.error('Setup finish error:', err);
|
||
await interaction.reply({
|
||
content: `Failed to send panel: ${err.message}`,
|
||
ephemeral: true
|
||
}).catch(() => {});
|
||
}
|
||
clearState(userId);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Handle setup modal submit (panel name).
|
||
*/
|
||
async function handleSetupModal(interaction) {
|
||
if (!interaction.customId.startsWith(PREFIX_MODAL)) return false;
|
||
|
||
const userId = interaction.user.id;
|
||
const state = getState(userId);
|
||
if (!state) {
|
||
await interaction.reply({
|
||
content: 'This setup session has expired. Run `/setup` again.',
|
||
ephemeral: true
|
||
}).catch(() => {});
|
||
return true;
|
||
}
|
||
|
||
if (interaction.customId === PREFIX_MODAL + 'name') {
|
||
const panelName = interaction.fields.getTextInputValue('panel_name').trim();
|
||
setState(userId, { panelName, step: 1 });
|
||
await interaction.deferReply({ ephemeral: true });
|
||
const payload = step1Embed(panelName);
|
||
await interaction.editReply(payload);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Handle setup select menus (roles, category, transcript channel, panel channel).
|
||
*/
|
||
async function handleSetupSelect(interaction) {
|
||
const customId = interaction.customId;
|
||
if (!customId.startsWith(PREFIX_SELECT)) return false;
|
||
|
||
const userId = interaction.user.id;
|
||
const state = getState(userId);
|
||
if (!state) {
|
||
await interaction.reply({
|
||
content: 'This setup session has expired. Run `/setup` again.',
|
||
ephemeral: true
|
||
}).catch(() => {});
|
||
return true;
|
||
}
|
||
|
||
if (customId === PREFIX_SELECT + 'roles') {
|
||
const roles = interaction.roles;
|
||
const roleIds = [...roles.keys()];
|
||
const roleLabels = [...roles.values()].map(r => r.name);
|
||
setState(userId, { roleIds, roleLabels });
|
||
await interaction.update(step2Embed(roleLabels));
|
||
return true;
|
||
}
|
||
|
||
if (customId === PREFIX_SELECT + 'category') {
|
||
const channel = interaction.channels.first();
|
||
setState(userId, {
|
||
categoryId: channel?.id,
|
||
categoryName: channel?.name
|
||
});
|
||
await interaction.update(step3Embed(getState(userId)));
|
||
return true;
|
||
}
|
||
if (customId === PREFIX_SELECT + 'thread_channel') {
|
||
const channel = interaction.channels.first();
|
||
setState(userId, {
|
||
threadChannelId: channel?.id,
|
||
threadChannelName: channel?.name
|
||
});
|
||
await interaction.update(step3Embed(getState(userId)));
|
||
return true;
|
||
}
|
||
|
||
if (customId === PREFIX_SELECT + 'transcript') {
|
||
const channel = interaction.channels.first();
|
||
setState(userId, {
|
||
transcriptChannelId: channel?.id,
|
||
transcriptChannelName: channel?.name
|
||
});
|
||
await interaction.update(step4Embed(channel?.name));
|
||
return true;
|
||
}
|
||
|
||
if (customId === PREFIX_SELECT + 'panel_channel') {
|
||
const channel = interaction.channels.first();
|
||
setState(userId, {
|
||
panelChannelId: channel?.id,
|
||
panelChannelName: channel?.name
|
||
});
|
||
await interaction.update(step5Embed(channel?.name));
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
module.exports = {
|
||
PREFIX_BUTTON,
|
||
PREFIX_MODAL,
|
||
PREFIX_SELECT,
|
||
handleSetupCommand,
|
||
handleSetupButton,
|
||
handleSetupModal,
|
||
handleSetupSelect
|
||
};
|