655
handlers/setup.js
Normal file
655
handlers/setup.js
Normal file
@@ -0,0 +1,655 @@
|
||||
/**
|
||||
* /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 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 channel.send({ 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
|
||||
};
|
||||
Reference in New Issue
Block a user