Sync broccolini-bot: rename from zammad, docs in docs/, security gitignore, remove zammad deps

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
samkintop
2026-02-12 02:56:00 -06:00
parent 08a16b4a75
commit 29a13768f7
37 changed files with 1093 additions and 3229 deletions

View File

@@ -15,11 +15,9 @@ const {
TextInputStyle
} = require('discord.js');
const { mongoose } = require('../db-connection');
const { CONFIG, ZAMMAD } = require('../config');
const { CONFIG } = require('../config');
const { canRename, makeTicketName, minutesFromMs, pickTicketCategoryId, createDiscordTicketAsThread, checkTicketCreationRateLimit } = require('../services/tickets');
const { sendTicketClosedEmail } = require('../services/gmail');
const { createZammadTicket, closeZammadTicket, ensureZammadUserForDiscordUser, updateZammadUser } = require('../services/zammad');
const { saveZammadId } = require('../services/tickets');
const { getTicketActionRow } = require('../utils/ticketComponents');
const { setEmailRouting } = require('../services/guildSettings');
const { runEscalation, runDeescalation } = require('./commands');
@@ -218,9 +216,9 @@ async function handleButton(interaction) {
}
// --- TAG DELETE CONFIRM ---
if (interaction.customId.startsWith('confirm_delete_tag_')) {
if (interaction.customId.startsWith('confirm_delete_tag::')) {
trackInteraction('buttons', 'confirm-delete-tag', interaction.user.tag);
const tagName = interaction.customId.replace('confirm_delete_tag_', '');
const tagName = interaction.customId.slice('confirm_delete_tag::'.length);
try {
const result = await Tag.deleteOne({ name: tagName });
@@ -331,9 +329,12 @@ async function handleClaim(interaction, ticket) {
.setLabel(label);
await interaction.update({ components: [row] });
const claimText = CONFIG.TICKET_CLAIMED_MESSAGE
.replace(/\{staff_mention\}/g, interaction.user.toString())
.replace(/\{staff_name\}/g, interaction.member?.displayName || interaction.user.username);
const claimEmbed = new EmbedBuilder()
.setDescription(`Ticket claimed by ${interaction.user.toString()}`)
.setColor(0x2ecc71);
.setDescription(claimText)
.setColor(CONFIG.EMBED_COLOR_INFO);
await interaction.followUp({ embeds: [claimEmbed] });
} else {
// Unclaim
@@ -378,9 +379,12 @@ async function handleClaim(interaction, ticket) {
.setLabel(CONFIG.BUTTON_LABEL_CLAIM);
await interaction.update({ components: [row] });
const unclaimText = CONFIG.TICKET_UNCLAIMED_MESSAGE
.replace(/\{staff_mention\}/g, interaction.user.toString())
.replace(/\{staff_name\}/g, interaction.member?.displayName || interaction.user.username);
const unclaimEmbed = new EmbedBuilder()
.setDescription(`Ticket unclaimed by ${interaction.user.toString()}`)
.setColor(0xf1c40f);
.setDescription(unclaimText)
.setColor(CONFIG.EMBED_COLOR_INFO);
await interaction.followUp({ embeds: [unclaimEmbed] });
}
}
@@ -410,40 +414,48 @@ async function handleConfirmClose(interaction, ticket) {
name: `transcript-${interaction.channel.name}.txt`
});
const channelName = interaction.channel.name;
const opened = new Date(ticket.createdAt);
const openedStr = opened.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'
});
// In-ticket message before transcript is posted (Discord close message)
const discordCloseContent = CONFIG.DISCORD_CLOSE_MESSAGE;
await interaction.channel.send(discordCloseContent);
const transcriptChan = await interaction.client.channels
.fetch(CONFIG.TRANSCRIPT_CHAN)
.catch(() => null);
let transcriptMsg = null;
const transcriptContent = CONFIG.DISCORD_TRANSCRIPT_MESSAGE
.replace(/\{channel_name\}/g, channelName)
.replace(/\{email\}/g, ticket.senderEmail || '')
.replace(/\{date_opened\}/g, openedStr)
.replace(/\{date_closed\}/g, closedStr)
+ `\n\nDate Opened: ${openedStr}\nDate Closed: ${closedStr}`;
if (transcriptChan) {
const opened = new Date(ticket.createdAt);
const openedStr = opened.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'
});
transcriptMsg = await transcriptChan.send({
content:
`Transcript: \`${ticket.senderEmail}\`\n` +
`Date Opened: ${openedStr}\n` +
`Date Closed: ${closedStr}`,
content: transcriptContent,
files: [file]
});
}
@@ -454,10 +466,15 @@ async function handleConfirmClose(interaction, ticket) {
try {
const creator = await interaction.client.users.fetch(creatorId);
const dmFile = new AttachmentBuilder(Buffer.from(log), {
name: `transcript-${interaction.channel.name}.txt`
name: `transcript-${channelName}.txt`
});
const dmContent = CONFIG.DISCORD_TRANSCRIPT_MESSAGE
.replace(/\{channel_name\}/g, channelName)
.replace(/\{email\}/g, ticket.senderEmail || '')
.replace(/\{date_opened\}/g, openedStr)
.replace(/\{date_closed\}/g, closedStr);
await creator.send({
content: `Your ticket **${interaction.channel.name}** has been closed. Here is your transcript:`,
content: dmContent,
files: [dmFile]
});
} catch (dmErr) {
@@ -471,7 +488,6 @@ async function handleConfirmClose(interaction, ticket) {
if (logChan) {
const closerMention = interaction.user.toString();
const closerDisplayName = interaction.member?.displayName || interaction.user.username;
const channelName = interaction.channel.name;
let logMsg;
if (ticket.gmailThreadId?.startsWith('discord-')) {
@@ -495,12 +511,6 @@ async function handleConfirmClose(interaction, ticket) {
if (!ticket.gmailThreadId?.startsWith('discord-')) {
await sendTicketClosedEmail(ticket, closerDisplayName);
}
if (ticket.zammadTicketId && ZAMMAD?.URL && ZAMMAD?.TOKEN) {
await closeZammadTicket(ticket.zammadTicketId).catch(zErr =>
console.error('Zammad close failed:', zErr.message)
);
}
await Ticket.updateOne(
{ gmailThreadId: ticket.gmailThreadId },
{ $set: { discordThreadId: null, status: 'closed' } }
@@ -599,53 +609,12 @@ async function handleTicketModal(interaction) {
lastActivity: now
});
// Create Zammad ticket for Discord-originated ticket
const displayName = interaction.member?.displayName || interaction.user.username;
try {
const zammadTicket = await createZammadTicket({
subject,
body: description,
email,
name: displayName,
gameName: game || 'Not specified',
gameKey: null,
group: ZAMMAD.DISCORD_GROUP,
discordUsername: displayName
});
if (zammadTicket?.id) {
await saveZammadId(gmailThreadId, zammadTicket.id);
}
// Update Zammad customer with Discord username and ID so they show in user/ticket views
if (zammadTicket?.customer_id) {
try {
await updateZammadUser(zammadTicket.customer_id, {
discord_username: displayName,
discord_id: interaction.user.id
});
} catch (_) {
/* custom attributes may not exist in Zammad */
}
}
} catch (zErr) {
console.error('Zammad ticket create (Discord ticket) failed:', zErr.response?.data || zErr.message);
}
// Ensure Zammad user if creator has a website account (keeps discord_id/discord_username in sync)
try {
const websiteUser = await User.findOne({ discordID: String(interaction.user.id) })
.select('email discordID firstname lastname')
.lean();
if (websiteUser?.email) {
await ensureZammadUserForDiscordUser(websiteUser, { discordUsername: displayName });
}
} catch (zErr) {
console.error('Zammad user ensure (Discord ticket) failed:', zErr.message);
}
// Welcome embed (green)
// Welcome embed (dark grey #1e2124)
const welcomeEmbed = new EmbedBuilder()
.setDescription("We got your ticket. We'll be with you as soon as possible.\nFeel free to add any additional information to your ticket.")
.setColor(0x2ecc71)
.setDescription(CONFIG.TICKET_WELCOME_MESSAGE)
.setColor(CONFIG.EMBED_COLOR_INFO)
.setFooter({ text: 'Indifferent Broccoli Tickets' });
// Ticket details embed (dark) short labels, trimmed description