huge changes

This commit is contained in:
indifferentketchup
2026-04-07 01:43:06 -05:00
parent ca63ecbcfd
commit 69c247ed1b
37 changed files with 3468 additions and 169 deletions

View File

@@ -20,8 +20,11 @@ const { getEmailRouting } = require('../services/guildSettings');
const { enqueueRename, enqueueMove } = require('../services/channelQueue');
const { setNotifyDm } = require('../services/staffSettings');
const { trackInteraction, trackError, getAnalyticsSummary } = require('./analytics');
const { logTicketEvent, logSecurity } = require('../services/debugLog');
const { handleAccountInfoCommand } = require('./accountinfo');
const { handleSetupCommand } = require('./setup');
const { pendingCloses } = require('./pendingCloses');
const { increment } = require('../services/patternStore');
const Ticket = mongoose.model('Ticket');
const Tag = mongoose.model('Tag');
@@ -55,6 +58,7 @@ async function requireStaffRole(interaction) {
content: `This command is only available to the support team (${roleMention}).`,
ephemeral: true
});
logSecurity('Unauthorized command attempt', interaction.user, interaction.commandName).catch(() => {});
return true;
}
@@ -75,6 +79,12 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
ticket.escalated = true;
ticket.escalationTier = nextTier;
ticket.claimedBy = null;
increment('escalations', ticket.game || 'unknown', 'today');
increment('escalations', ticket.game || 'unknown', 'week');
increment('user_escalations', ticket.senderEmail, 'week');
increment('staff_escalations', interaction.user.id, 'today');
increment('staff_escalations', interaction.user.id, 'week');
if (ticket.game) increment(`staff_game_escalations:${interaction.user.id}`, ticket.game, 'week');
const creatorNickname = await resolveCreatorNickname(interaction.guild, ticket);
const renameInfo = await canRename(ticket);
@@ -124,12 +134,17 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
.setFooter({ text: `Escalated by ${interaction.member?.displayName || interaction.user.username}` });
const updatedTicketForRow = { ...ticket, escalationTier: nextTier, escalated: true };
const escalationRow = getTicketActionRow(updatedTicketForRow);
await interaction.channel.send({
const escalationMsg = await interaction.channel.send({
content: null,
embeds: [escalatedEmbed],
components: [escalationRow]
});
if (CONFIG.PIN_ESCALATION_MESSAGE_ENABLED && escalationMsg) {
const { pinMessage } = require('../services/pinMessage');
await pinMessage(escalationMsg, interaction.client).catch(() => {});
}
if (!isDiscordTicket && ticket.gmailThreadId) {
try {
const emailBody = CONFIG.ESCALATION_MESSAGE.replace(/\{support_name\}/g, CONFIG.SUPPORT_NAME) + (reason ? `\n\nReason: ${reason}` : '');
@@ -144,12 +159,16 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
}
}
if (nextTier === 2 && ticket.welcomeMessageId) {
try {
const welcomeMsg = await interaction.channel.messages.fetch(ticket.welcomeMessageId);
await welcomeMsg.edit({ components: [getTicketActionRow(updatedTicketForRow)] });
} catch (e) {
console.error('Failed to update welcome message after escalate:', e.message);
if (nextTier === 2) {
if (!ticket.welcomeMessageId) {
console.warn('welcomeMessageId is null/undefined; skipping welcome-message update for escalation');
} else {
try {
const welcomeMsg = await interaction.channel.messages.fetch(ticket.welcomeMessageId);
await welcomeMsg.edit({ components: [getTicketActionRow(updatedTicketForRow)] });
} catch (e) {
console.error('Failed to update welcome message after escalate:', e.message);
}
}
}
@@ -376,6 +395,7 @@ async function handleCommand(interaction) {
// /staffnotification (admin only)
if (interaction.commandName === 'staffnotification') {
if (interaction.user.id !== CONFIG.ADMIN_ID) {
logSecurity('Unauthorized command attempt', interaction.user, interaction.commandName).catch(() => {});
return interaction.reply({ content: 'This command is restricted to the bot admin.', ephemeral: true });
}
const member = interaction.options.getMember('member');
@@ -540,6 +560,79 @@ async function handleCommand(interaction) {
}
}
// /gmailpoll
// /staffthread
if (interaction.commandName === 'staffthread') {
const sub = interaction.options.getSubcommand();
if (sub === 'toggle') {
CONFIG.STAFF_THREAD_ENABLED = !CONFIG.STAFF_THREAD_ENABLED;
return interaction.reply({ content: `Staff threads are now **${CONFIG.STAFF_THREAD_ENABLED ? 'enabled' : 'disabled'}**.`, ephemeral: true });
}
if (sub === 'name') {
const name = interaction.options.getString('thread_name').slice(0, 100);
CONFIG.STAFF_THREAD_NAME = name;
return interaction.reply({ content: `Staff thread name set to **${name}**.`, ephemeral: true });
}
if (sub === 'autorole') {
const enabled = interaction.options.getBoolean('enabled');
CONFIG.STAFF_THREAD_AUTO_ADD_ROLE = enabled;
return interaction.reply({ content: `Auto-add role to staff thread is now **${enabled ? 'enabled' : 'disabled'}**.`, ephemeral: true });
}
return;
}
// /pinmessages
if (interaction.commandName === 'pinmessages') {
const sub = interaction.options.getSubcommand();
const enabled = interaction.options.getBoolean('enabled');
if (sub === 'initial') {
CONFIG.PIN_INITIAL_MESSAGE_ENABLED = enabled;
return interaction.reply({ content: `Auto-pin initial message is now **${enabled ? 'enabled' : 'disabled'}**.`, ephemeral: true });
}
if (sub === 'escalation') {
CONFIG.PIN_ESCALATION_MESSAGE_ENABLED = enabled;
return interaction.reply({ content: `Auto-pin escalation message is now **${enabled ? 'enabled' : 'disabled'}**.`, ephemeral: true });
}
if (sub === 'suppress') {
CONFIG.PIN_SUPPRESS_SYSTEM_MESSAGE = enabled;
return interaction.reply({ content: `Suppress pin system message is now **${enabled ? 'enabled' : 'disabled'}**.`, ephemeral: true });
}
return;
}
if (interaction.commandName === 'gmailpoll') {
const seconds = parseInt(interaction.options.getString('interval'), 10);
const { setGmailPollInterval } = require('../broccolini-discord');
setGmailPollInterval(seconds * 1000);
logTicketEvent('Gmail poll interval updated', [{ name: 'Interval', value: `${seconds}s` }, { name: 'Set by', value: interaction.user.tag }], interaction).catch(() => {});
return interaction.reply({ content: `Gmail poll interval set to ${seconds} seconds.`, ephemeral: true });
}
// /closetimer
if (interaction.commandName === 'closetimer') {
const seconds = parseInt(interaction.options.getString('seconds'), 10);
CONFIG.FORCE_CLOSE_TIMER = seconds;
logTicketEvent('Close timer updated', [{ name: 'Duration', value: `${seconds}s` }, { name: 'Set by', value: interaction.user.tag }], interaction).catch(() => {});
return interaction.reply({ content: `Force-close timer set to ${seconds} seconds.`, ephemeral: true });
}
// /cancel-close
if (interaction.commandName === 'cancel-close') {
const pending = pendingCloses.get(interaction.channel.id);
if (!pending) {
return interaction.reply({ content: 'No pending close for this channel.', ephemeral: true });
}
clearTimeout(pending.timeout);
const { logTicketEvent } = require('../services/debugLog');
logTicketEvent('Force-close cancelled', [
{ name: 'Ticket', value: interaction.channel.name || interaction.channel.id },
{ name: 'Cancelled by', value: interaction.user.tag },
{ name: 'Original setter', value: pending.username || 'Unknown' }
], interaction).catch(() => {});
pendingCloses.delete(interaction.channel.id);
return interaction.reply({ content: 'Close cancelled.', ephemeral: true });
}
// /force-close
if (interaction.commandName === 'force-close') {
const ticket = await Ticket.findOne({ discordThreadId: interaction.channel.id }).lean();
@@ -547,71 +640,86 @@ async function handleCommand(interaction) {
return interaction.reply({ content: 'This channel is not linked to a ticket.', ephemeral: true });
}
try {
await Ticket.updateOne(
{ gmailThreadId: ticket.gmailThreadId },
{ $set: { status: 'closed' } }
);
if (pendingCloses.has(interaction.channel.id)) {
return interaction.reply({ content: 'A close is already pending for this ticket.', ephemeral: true });
}
await interaction.reply('Ticket force-closed. Archiving...');
const timerSeconds = CONFIG.FORCE_CLOSE_TIMER;
await interaction.reply(`Closing ticket in ${timerSeconds} seconds. Use \`/cancel-close\` to abort.`);
const channelRef = interaction.channel;
const clientRef = interaction.client;
const timerId = setTimeout(async () => {
pendingCloses.delete(channelRef.id);
const freshTicket = await Ticket.findOne({ discordThreadId: channelRef.id }).lean();
if (!freshTicket || freshTicket.status === 'closed') return;
try {
await interaction.channel.send(CONFIG.DISCORD_CLOSE_MESSAGE);
await Ticket.updateOne(
{ gmailThreadId: freshTicket.gmailThreadId },
{ $set: { status: 'closed' } }
);
const messages = await interaction.channel.messages.fetch({ limit: 100 });
const log =
`TRANSCRIPT: ${ticket.subject}\nUser: ${ticket.senderEmail}\n---\n` +
messages
.reverse()
.map(m => `[${m.createdAt.toLocaleString()}] ${m.author.tag}: ${m.cleanContent}`)
.join('\n');
await channelRef.send('Ticket force-closed. Archiving...');
const file = new AttachmentBuilder(Buffer.from(log), {
name: `transcript-${interaction.channel.name}.txt`
});
const transcriptChan = await interaction.client.channels
.fetch(CONFIG.TRANSCRIPT_CHAN)
.catch(() => null);
if (transcriptChan) {
const closedAt = new Date();
const openedStr = new Date(ticket.createdAt).toLocaleString('en-US', {
month: '2-digit', day: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: true, timeZoneName: 'short'
});
const closedStr = closedAt.toLocaleString('en-US', {
month: '2-digit', day: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: true, timeZoneName: 'short'
});
const transcriptContent = CONFIG.DISCORD_TRANSCRIPT_MESSAGE
.replace(/\{channel_name\}/g, interaction.channel.name)
.replace(/\{email\}/g, ticket.senderEmail || '')
.replace(/\{date_opened\}/g, openedStr)
.replace(/\{date_closed\}/g, closedStr)
+ `\n\nDate Opened: ${openedStr}\nDate Closed: ${closedStr}`;
await transcriptChan.send({
content: transcriptContent,
files: [file]
});
}
} catch (tErr) {
console.error('Transcript error (force-close):', tErr);
}
setTimeout(async () => {
try {
await interaction.channel.delete('Ticket force-closed');
} catch (e) {
console.error('Failed to delete channel:', e);
await channelRef.send(CONFIG.DISCORD_CLOSE_MESSAGE);
const messages = await channelRef.messages.fetch({ limit: 100 });
const log =
`TRANSCRIPT: ${freshTicket.subject}\nUser: ${freshTicket.senderEmail}\n---\n` +
messages
.reverse()
.map(m => `[${m.createdAt.toLocaleString()}] ${m.author.tag}: ${m.cleanContent}`)
.join('\n');
const file = new AttachmentBuilder(Buffer.from(log), {
name: `transcript-${channelRef.name}.txt`
});
const transcriptChan = await clientRef.channels
.fetch(CONFIG.TRANSCRIPT_CHAN)
.catch(() => null);
if (transcriptChan) {
const closedAt = new Date();
const openedStr = new Date(freshTicket.createdAt).toLocaleString('en-US', {
month: '2-digit', day: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: true, timeZoneName: 'short'
});
const closedStr = closedAt.toLocaleString('en-US', {
month: '2-digit', day: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: true, timeZoneName: 'short'
});
const transcriptContent = CONFIG.DISCORD_TRANSCRIPT_MESSAGE
.replace(/\{channel_name\}/g, channelRef.name)
.replace(/\{email\}/g, freshTicket.senderEmail || '')
.replace(/\{date_opened\}/g, openedStr)
.replace(/\{date_closed\}/g, closedStr)
+ `\n\nDate Opened: ${openedStr}\nDate Closed: ${closedStr}`;
await transcriptChan.send({
content: transcriptContent,
files: [file]
});
}
} catch (tErr) {
console.error('Transcript error (force-close):', tErr);
}
}, 5000);
} catch (err) {
console.error('Force close error:', err);
await interaction.reply({ content: 'Failed to close ticket.', ephemeral: true });
}
setTimeout(async () => {
try {
await channelRef.delete('Ticket force-closed');
} catch (e) {
console.error('Failed to delete channel:', e);
}
}, 5000);
} catch (err) {
console.error('Force close error:', err);
}
}, timerSeconds * 1000);
pendingCloses.set(channelRef.id, { timeout: timerId, userId: interaction.user.id, username: interaction.user.tag });
}
// /topic
@@ -649,6 +757,9 @@ async function handleCommand(interaction) {
const emoji = tagEntry ? tagEntry.emoji : '';
const channelMessage = `Your ticket has been categorized as ${emoji} **${tagEntry ? tagEntry.name : categoryValue}** ${emoji}.`;
await interaction.reply(channelMessage);
increment('tag_usage', categoryValue, 'today');
increment('tag_usage', categoryValue, 'week');
if (ticket.game) increment(`tag_game:${categoryValue}`, ticket.game, 'week');
} catch (err) {
trackError('tag-command', err, interaction);
await interaction.reply({ content: 'Failed to set ticket category.', ephemeral: true });
@@ -1189,16 +1300,20 @@ async function handleContextMenu(interaction) {
const row = getTicketActionRow({ escalationTier: 0 });
const welcomeMsg = await channel.send({
content: `<@&${CONFIG.ROLE_ID_TO_PING}>\nHey There ${message.author} 🥦`,
embeds: [welcomeEmbed, infoEmbed],
components: [row]
});
try {
const welcomeMsg = await channel.send({
content: `<@&${CONFIG.ROLE_ID_TO_PING}>\nHey There ${message.author} 🥦`,
embeds: [welcomeEmbed, infoEmbed],
components: [row]
});
await Ticket.updateOne(
{ discordThreadId: channel.id },
{ $set: { welcomeMessageId: welcomeMsg.id } }
);
await Ticket.updateOne(
{ discordThreadId: channel.id },
{ $set: { welcomeMessageId: welcomeMsg.id } }
);
} catch (err) {
console.error('welcomeMessageId-save', err);
}
await interaction.editReply(`✅ Ticket created: ${channel}`);
} catch (err) {