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:
@@ -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
|
||||
|
||||
@@ -11,10 +11,9 @@ const {
|
||||
PermissionFlagsBits
|
||||
} = require('discord.js');
|
||||
const { mongoose } = require('../db-connection');
|
||||
const { CONFIG, ZAMMAD, TICKET_TAGS } = require('../config');
|
||||
const { CONFIG, TICKET_TAGS } = require('../config');
|
||||
const { getPriorityEmoji, replaceVariables, escapeRegex } = require('../utils');
|
||||
const { canRename, makeTicketName, pickTicketCategoryId, createDiscordTicketAsThread, checkTicketCreationRateLimit } = require('../services/tickets');
|
||||
const { closeZammadTicket, ensureZammadUserForDiscordUser } = require('../services/zammad');
|
||||
const { sendTicketNotificationEmail } = require('../services/gmail');
|
||||
const { getTicketActionRow } = require('../utils/ticketComponents');
|
||||
const { getEmailRouting } = require('../services/guildSettings');
|
||||
@@ -26,6 +25,36 @@ const Ticket = mongoose.model('Ticket');
|
||||
const Tag = mongoose.model('Tag');
|
||||
const User = mongoose.model('User');
|
||||
|
||||
/**
|
||||
* True if member has the support role (ROLE_ID_TO_PING) or any ADDITIONAL_STAFF_ROLES.
|
||||
* Used to restrict commands to staff only; customers cannot use bot commands.
|
||||
* @param {import('discord.js').GuildMember|null} member
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasStaffRole(member) {
|
||||
if (!member?.roles?.cache) return false;
|
||||
if (CONFIG.ROLE_ID_TO_PING && member.roles.cache.has(CONFIG.ROLE_ID_TO_PING)) return true;
|
||||
const additional = CONFIG.ADDITIONAL_STAFF_ROLES || [];
|
||||
return additional.some(roleId => member.roles.cache.has(roleId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply ephemeral and return true if the interaction is in a guild and the user is not staff (so caller should return).
|
||||
* @param {import('discord.js').CommandInteraction|import('discord.js').ContextMenuCommandInteraction} interaction
|
||||
* @returns {Promise<boolean>} true if caller should return (user is not allowed)
|
||||
*/
|
||||
async function requireStaffRole(interaction) {
|
||||
if (!interaction.guild) return false;
|
||||
if (!CONFIG.ROLE_ID_TO_PING && (!CONFIG.ADDITIONAL_STAFF_ROLES || CONFIG.ADDITIONAL_STAFF_ROLES.length === 0)) return false;
|
||||
if (hasStaffRole(interaction.member)) return false;
|
||||
const roleMention = CONFIG.ROLE_ID_TO_PING ? `<@&${CONFIG.ROLE_ID_TO_PING}>` : 'support';
|
||||
await interaction.reply({
|
||||
content: `This command is only available to the support team (${roleMention}).`,
|
||||
ephemeral: true
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run escalation to a target DB tier (1 = tier 2, 2 = tier 3). Caller must validate ticket and currentTier < nextTier.
|
||||
*/
|
||||
@@ -69,7 +98,7 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
|
||||
|
||||
const pendingEmbed = new EmbedBuilder()
|
||||
.setDescription('Ticket will be escalated in a few seconds.')
|
||||
.setColor(0xe74c3c);
|
||||
.setColor(CONFIG.EMBED_COLOR_INFO);
|
||||
await interaction.reply({ content: null, embeds: [pendingEmbed] });
|
||||
|
||||
const creatorId = isDiscordTicket
|
||||
@@ -84,12 +113,12 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
|
||||
`${heyLine}\n**Getting the senior ${roleMention} for you.**`
|
||||
);
|
||||
|
||||
const seniorLine = `A senior ${CONFIG.SUPPORT_NAME} will be here to assist as soon as possible.`;
|
||||
const escalationBody = CONFIG.ESCALATION_MESSAGE
|
||||
.replace(/\{support_name\}/g, CONFIG.SUPPORT_NAME)
|
||||
+ (reason ? `\n\n**Reason:** ${reason}` : '');
|
||||
const escalatedEmbed = new EmbedBuilder()
|
||||
.setDescription(
|
||||
`${CONFIG.ESCALATION_MESSAGE}\n\n${seniorLine}${reason ? `\n\n**Reason:** ${reason}` : ''}`
|
||||
)
|
||||
.setColor(0x2ecc71);
|
||||
.setDescription(escalationBody)
|
||||
.setColor(CONFIG.EMBED_COLOR_INFO);
|
||||
const updatedTicketForRow = { ...ticket, escalationTier: nextTier, escalated: true };
|
||||
const escalationRow = getTicketActionRow(updatedTicketForRow);
|
||||
await interaction.channel.send({
|
||||
@@ -99,7 +128,7 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
|
||||
});
|
||||
|
||||
if (!isDiscordTicket && ticket.gmailThreadId) {
|
||||
const emailBody = `${CONFIG.ESCALATION_MESSAGE}\n\n${seniorLine}${reason ? `\n\nReason: ${reason}` : ''}`;
|
||||
const emailBody = CONFIG.ESCALATION_MESSAGE.replace(/\{support_name\}/g, CONFIG.SUPPORT_NAME) + (reason ? `\n\nReason: ${reason}` : '');
|
||||
await sendTicketNotificationEmail(
|
||||
ticket,
|
||||
`Ticket escalated to ${nextTier === 1 ? 'tier 2' : 'tier 3'}`,
|
||||
@@ -199,6 +228,9 @@ async function runDeescalation(interaction, ticket) {
|
||||
* Main slash-command handler.
|
||||
*/
|
||||
async function handleCommand(interaction) {
|
||||
// Only /help can be used by everyone; all other commands require staff role (ROLE_ID_TO_PING / ADDITIONAL_STAFF_ROLES)
|
||||
if (interaction.commandName !== 'help' && (await requireStaffRole(interaction))) return;
|
||||
|
||||
// /setup
|
||||
if (interaction.commandName === 'setup') {
|
||||
return handleSetupCommand(interaction);
|
||||
@@ -415,8 +447,9 @@ async function handleCommand(interaction) {
|
||||
|
||||
await interaction.reply('Ticket force-closed. Archiving...');
|
||||
|
||||
// Generate transcript inline (same as confirm_close)
|
||||
try {
|
||||
await interaction.channel.send(CONFIG.DISCORD_CLOSE_MESSAGE);
|
||||
|
||||
const messages = await interaction.channel.messages.fetch({ limit: 100 });
|
||||
const log =
|
||||
`TRANSCRIPT: ${ticket.subject}\nUser: ${ticket.senderEmail}\n---\n` +
|
||||
@@ -434,8 +467,25 @@ async function handleCommand(interaction) {
|
||||
.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: `Force-closed transcript: \`${ticket.senderEmail}\``,
|
||||
content: transcriptContent,
|
||||
files: [file]
|
||||
});
|
||||
}
|
||||
@@ -443,10 +493,6 @@ async function handleCommand(interaction) {
|
||||
console.error('Transcript error (force-close):', tErr);
|
||||
}
|
||||
|
||||
if (ticket.zammadTicketId && ZAMMAD.URL && ZAMMAD.TOKEN) {
|
||||
await closeZammadTicket(ticket.zammadTicketId);
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await interaction.channel.delete('Ticket force-closed');
|
||||
@@ -567,9 +613,11 @@ async function handleCommand(interaction) {
|
||||
|
||||
else if (subcommand === 'delete') {
|
||||
const name = interaction.options.getString('name');
|
||||
// Use :: delimiter so tag names with underscores are parsed correctly (Discord customId max 100 chars)
|
||||
const customId = `confirm_delete_tag::${name}`.slice(0, 100);
|
||||
const confirmRow = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`confirm_delete_tag_${name}`)
|
||||
.setCustomId(customId)
|
||||
.setLabel('Yes, Delete Tag')
|
||||
.setStyle(ButtonStyle.Danger),
|
||||
new ButtonBuilder()
|
||||
@@ -726,12 +774,12 @@ async function handleCommand(interaction) {
|
||||
new ButtonBuilder()
|
||||
.setCustomId('open_ticket_thread')
|
||||
.setLabel('Create ticket (thread)')
|
||||
.setStyle(ButtonStyle.Success)
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setEmoji('🧵'),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('open_ticket_channel')
|
||||
.setLabel('Create ticket (channel)')
|
||||
.setStyle(ButtonStyle.Success)
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setEmoji('📁')
|
||||
);
|
||||
} else if (panelType === 'thread') {
|
||||
@@ -739,7 +787,7 @@ async function handleCommand(interaction) {
|
||||
new ButtonBuilder()
|
||||
.setCustomId('open_ticket_thread')
|
||||
.setLabel('Create ticket')
|
||||
.setStyle(ButtonStyle.Success)
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setEmoji('🧵')
|
||||
);
|
||||
} else if (panelType === 'category') {
|
||||
@@ -747,7 +795,7 @@ async function handleCommand(interaction) {
|
||||
new ButtonBuilder()
|
||||
.setCustomId('open_ticket_channel')
|
||||
.setLabel('Create ticket')
|
||||
.setStyle(ButtonStyle.Success)
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setEmoji('📁')
|
||||
);
|
||||
} else {
|
||||
@@ -755,7 +803,7 @@ async function handleCommand(interaction) {
|
||||
new ButtonBuilder()
|
||||
.setCustomId('open_ticket')
|
||||
.setLabel('Create ticket')
|
||||
.setStyle(ButtonStyle.Success)
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setEmoji('✅')
|
||||
);
|
||||
}
|
||||
@@ -925,6 +973,9 @@ async function handleCommand(interaction) {
|
||||
* Context menu interaction handler.
|
||||
*/
|
||||
async function handleContextMenu(interaction) {
|
||||
// Restrict all guild context menus to staff role only
|
||||
if (await requireStaffRole(interaction)) return;
|
||||
|
||||
// Create Ticket From Message
|
||||
if (interaction.isMessageContextMenuCommand() && interaction.commandName === 'Create Ticket From Message') {
|
||||
trackInteraction('contextMenus', 'create-ticket-from-message', interaction.user.tag);
|
||||
@@ -991,20 +1042,9 @@ async function handleContextMenu(interaction) {
|
||||
lastActivity: now
|
||||
});
|
||||
|
||||
try {
|
||||
const websiteUser = await User.findOne({ discordID: String(message.author.id) })
|
||||
.select('email discordID firstname lastname')
|
||||
.lean();
|
||||
if (websiteUser?.email) {
|
||||
await ensureZammadUserForDiscordUser(websiteUser);
|
||||
}
|
||||
} catch (zErr) {
|
||||
console.error('Zammad user ensure (Discord ticket from message) failed:', zErr.message);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const infoEmbed = new EmbedBuilder()
|
||||
.setColor(CONFIG.EMBED_COLOR_INFO)
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
/**
|
||||
* Discord messageCreate handler – forwards staff replies to Gmail and Zammad.
|
||||
* Discord messageCreate handler – forwards staff replies to Gmail.
|
||||
*/
|
||||
const { mongoose } = require('../db-connection');
|
||||
const { CONFIG } = require('../config');
|
||||
const { ZAMMAD } = require('../config');
|
||||
const { extractRawEmail } = require('../utils');
|
||||
const { getGmailClient, sendGmailReply } = require('../services/gmail');
|
||||
const { addZammadArticle } = require('../services/zammad');
|
||||
const { updateTicketActivity } = require('../services/tickets');
|
||||
|
||||
const Ticket = mongoose.model('Ticket');
|
||||
|
||||
/**
|
||||
* Handle a Discord message in a ticket channel → relay to Gmail (email tickets only) + Zammad.
|
||||
* Handle a Discord message in a ticket channel → relay to Gmail (email tickets only).
|
||||
*/
|
||||
async function handleDiscordReply(m) {
|
||||
if (m.author.bot || m.interaction) return;
|
||||
@@ -22,19 +20,11 @@ async function handleDiscordReply(m) {
|
||||
|
||||
const discordUser = m.member?.displayName || m.author.username;
|
||||
|
||||
// Discord-originated tickets: no Gmail thread; only add reply to Zammad.
|
||||
if (ticket.gmailThreadId.startsWith('discord-')) {
|
||||
if (ticket.zammadTicketId && ZAMMAD.URL && ZAMMAD.TOKEN) {
|
||||
try {
|
||||
await addZammadArticle(ticket.zammadTicketId, m.content, { from: discordUser });
|
||||
} catch (zErr) {
|
||||
console.error('Zammad article (Discord ticket reply) failed:', zErr.response?.data || zErr.message);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Email tickets: send reply via Gmail and add to Zammad.
|
||||
// Email tickets: send reply via Gmail.
|
||||
try {
|
||||
const gmail = getGmailClient();
|
||||
const thread = await gmail.users.threads.get({
|
||||
@@ -78,14 +68,6 @@ async function handleDiscordReply(m) {
|
||||
);
|
||||
|
||||
await updateTicketActivity(ticket.gmailThreadId);
|
||||
|
||||
if (ticket.zammadTicketId && ZAMMAD.URL && ZAMMAD.TOKEN) {
|
||||
try {
|
||||
await addZammadArticle(ticket.zammadTicketId, m.content, { from: discordUser });
|
||||
} catch (zErr) {
|
||||
console.error('Zammad article (Discord reply) failed:', zErr.response?.data || zErr.message);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('REPLY ERROR:', e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user