Dynamic overflow categories

This commit is contained in:
indifferentketchup
2026-03-28 20:55:36 -05:00
parent 6b4fd65d4b
commit 1496a96274
10 changed files with 679 additions and 584 deletions

View File

@@ -16,7 +16,7 @@ const {
} = require('discord.js');
const { mongoose } = require('../db-connection');
const { CONFIG } = require('../config');
const { canRename, makeTicketName, minutesFromMs, pickTicketCategoryId, createDiscordTicketAsThread, checkTicketCreationRateLimit } = require('../services/tickets');
const { canRename, makeTicketName, minutesFromMs, getOrCreateTicketCategory, cleanupEmptyOverflowCategory, createDiscordTicketAsThread, checkTicketCreationRateLimit } = require('../services/tickets');
const { sendTicketClosedEmail } = require('../services/gmail');
const { getTicketActionRow } = require('../utils/ticketComponents');
const { setEmailRouting } = require('../services/guildSettings');
@@ -174,10 +174,13 @@ async function handleButton(interaction) {
return interaction.reply({ content: 'Tier 2 (ESCALATED2) is not configured for this ticket type.', ephemeral: true });
}
try {
await interaction.deferReply();
await runEscalation(interaction, ticket, 1, 'Escalated via button (Tier 2)');
} catch (err) {
trackError('escalate-button-tier2', err, interaction);
await interaction.reply({ content: 'Failed to escalate to tier 2.', ephemeral: true }).catch(() => {});
await interaction.editReply({ content: 'Failed to escalate to tier 2.' }).catch(() =>
interaction.followUp({ content: 'Failed to escalate to tier 2.', ephemeral: true }).catch(() => {})
);
}
return;
}
@@ -194,10 +197,13 @@ async function handleButton(interaction) {
return interaction.reply({ content: 'Tier 3 (ESCALATED3) is not configured for this ticket type.', ephemeral: true });
}
try {
await interaction.deferReply();
await runEscalation(interaction, ticket, 2, 'Escalated via button (Tier 3)');
} catch (err) {
trackError('escalate-button-tier3', err, interaction);
await interaction.reply({ content: 'Failed to escalate to tier 3.', ephemeral: true }).catch(() => {});
await interaction.editReply({ content: 'Failed to escalate to tier 3.' }).catch(() =>
interaction.followUp({ content: 'Failed to escalate to tier 3.', ephemeral: true }).catch(() => {})
);
}
return;
}
@@ -209,10 +215,13 @@ async function handleButton(interaction) {
return interaction.reply({ content: 'This ticket is not escalated.', ephemeral: true });
}
try {
await interaction.deferReply({ ephemeral: true });
await runDeescalation(interaction, ticket);
} catch (err) {
trackError('deescalate-button', err, interaction);
await interaction.reply({ content: 'Failed to deescalate this ticket.', ephemeral: true }).catch(() => {});
await interaction.editReply({ content: 'Failed to deescalate this ticket.' }).catch(() =>
interaction.followUp({ content: 'Failed to deescalate this ticket.', ephemeral: true }).catch(() => {})
);
}
return;
}
@@ -569,10 +578,20 @@ async function handleConfirmClose(interaction, ticket) {
});
}
const parentCatId = ticket.parentCategoryId;
const guildRef = interaction.guild;
setTimeout(
() => interaction.channel.delete().catch(() => {}),
5000
);
setTimeout(() => {
(async () => {
if (parentCatId && guildRef) {
await cleanupEmptyOverflowCategory(guildRef, parentCatId, CONFIG.TICKET_CATEGORY_NAME);
}
})();
}, 6000);
} catch (e) {
console.error('Close ticket error:', e);
}
@@ -606,9 +625,11 @@ async function handleTicketModal(interaction) {
const ticketNumber = (lastTicket?.ticketNumber || 0) + 1;
let channel;
let parentCategoryIdForTicket = null;
if (useThread && CONFIG.DISCORD_THREAD_CHANNEL_ID) {
try {
channel = await createDiscordTicketAsThread(guild, ticketNumber, interaction.user.id);
parentCategoryIdForTicket = channel.parent?.parentId ?? null;
} catch (err) {
console.error('Discord ticket thread create failed:', err.message);
return interaction.editReply('Could not create ticket thread. Check DISCORD_THREAD_CHANNEL_ID and try again.');
@@ -616,27 +637,39 @@ async function handleTicketModal(interaction) {
} else if (useThread && !CONFIG.DISCORD_THREAD_CHANNEL_ID) {
return interaction.editReply('Thread tickets are not configured (DISCORD_THREAD_CHANNEL_ID is not set). Use a channel panel or set the env variable.');
} else {
const categoryIds = [CONFIG.DISCORD_TICKET_CATEGORY_ID, ...(CONFIG.DISCORD_TICKET_OVERFLOW_CATEGORY_IDS || [])];
const parentId = pickTicketCategoryId(guild, categoryIds);
if (!parentId) {
return interaction.editReply('Discord ticket category not found or all categories full (50 channels max). Contact an administrator.');
let parentId;
try {
parentId = await getOrCreateTicketCategory(
guild,
CONFIG.DISCORD_TICKET_CATEGORY_ID,
CONFIG.TICKET_CATEGORY_NAME
);
} catch (err) {
console.error('getOrCreateTicketCategory (ticket modal):', err);
return interaction.editReply('Discord ticket category could not be resolved. Contact an administrator.');
}
parentCategoryIdForTicket = parentId;
try {
channel = await guild.channels.create({
name: `ticket-${ticketNumber}`,
type: ChannelType.GuildText,
parent: parentId,
permissionOverwrites: [
{ id: guild.id, deny: [PermissionFlagsBits.ViewChannel] },
{
id: interaction.user.id,
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory]
},
{
id: CONFIG.ROLE_ID_TO_PING,
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory]
}
]
});
} catch (err) {
console.error('guild.channels.create (ticket modal):', err);
return interaction.editReply('Failed to create ticket channel. Contact an administrator.');
}
channel = await guild.channels.create({
name: `ticket-${ticketNumber}`,
type: ChannelType.GuildText,
parent: parentId,
permissionOverwrites: [
{ id: guild.id, deny: [PermissionFlagsBits.ViewChannel] },
{
id: interaction.user.id,
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory]
},
{
id: CONFIG.ROLE_ID_TO_PING,
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory]
}
]
});
}
const gmailThreadId = `discord-${Date.now()}-${interaction.user.id}`;
@@ -651,7 +684,8 @@ async function handleTicketModal(interaction) {
status: 'open',
ticketNumber,
priority,
lastActivity: now
lastActivity: now,
parentCategoryId: parentCategoryIdForTicket
});
const displayName = interaction.member?.displayName || interaction.user.username;