diff --git a/broccolini-discord.js b/broccolini-discord.js index b72cca3..20f4f1e 100644 --- a/broccolini-discord.js +++ b/broccolini-discord.js @@ -99,6 +99,40 @@ client.on('interactionCreate', async interaction => { if (handled) return; } + if (interaction.isModalSubmit() && interaction.customId.startsWith('signature_modal_')) { + // Handle signature modal submit + try { + const valediction = interaction.fields.getTextInputValue('valediction'); + const displayName = interaction.fields.getTextInputValue('display_name'); + const tagline = interaction.fields.getTextInputValue('tagline'); + + const StaffSignature = mongoose.model('StaffSignature'); + await StaffSignature.findOneAndUpdate( + { userId: interaction.user.id }, + { + userId: interaction.user.id, + valediction, + displayName, + tagline, + updatedAt: new Date() + }, + { upsert: true, new: true } + ); + + await interaction.reply({ + content: 'Signature settings saved successfully!', + ephemeral: true + }); + } catch (err) { + console.error('Signature modal submit error:', err); + await interaction.reply({ + content: 'Failed to save signature settings.', + ephemeral: true + }); + } + return; + } + if (interaction.isModalSubmit() && ['ticket_modal', 'ticket_modal_thread', 'ticket_modal_channel'].includes(interaction.customId)) { return handleTicketModal(interaction); } diff --git a/commands/register.js b/commands/register.js index c525063..2edc0b1 100644 --- a/commands/register.js +++ b/commands/register.js @@ -572,8 +572,14 @@ async function registerCommands() { .addUserOption(opt => opt.setName('user').setDescription('Discord user').setRequired(true) ) - ) - ]; + ), + + new SlashCommandBuilder() + .setName('signature') + .setDescription('Set your personal email signature (valediction, display name, tagline)') + .setContexts([InteractionContextType.Guild]) + .setIntegrationTypes([ApplicationIntegrationType.GuildInstall]) + .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) const contextMenuCommands = [ new ContextMenuCommandBuilder() diff --git a/handlers/commands.js b/handlers/commands.js index bc7c989..2191b04 100644 --- a/handlers/commands.js +++ b/handlers/commands.js @@ -874,6 +874,57 @@ async function handleCommand(interaction) { } } + // /signature + if (interaction.commandName === 'signature') { + try { + await interaction.deferReply({ ephemeral: true }); + + // Fetch existing signature data if it exists + const StaffSignature = mongoose.model('StaffSignature'); + const existingSignature = await StaffSignature.findOne({ userId: interaction.user.id }).lean(); + + // Create modal + const { ModalBuilder, ActionRowBuilder, TextInputBuilder, TextInputStyle } = require('discord.js'); + const modal = new ModalBuilder() + .setCustomId(`signature_modal_${interaction.user.id}`) + .setTitle('Staff Signature Settings'); + + // Add text inputs to modal + const valedictionInput = new TextInputBuilder() + .setCustomId('valediction') + .setLabel('Valediction (e.g. "Best regards", "Thanks")') + .setStyle(TextInputStyle.Short) + .setRequired(false) + .setValue(existingSignature?.valediction || ''); + + const displayNameInput = new TextInputBuilder() + .setCustomId('display_name') + .setLabel('Display Name (e.g. "Support Team")') + .setStyle(TextInputStyle.Short) + .setRequired(false) + .setValue(existingSignature?.displayName || ''); + + const taglineInput = new TextInputBuilder() + .setCustomId('tagline') + .setLabel('Tagline (e.g. "Technical Support Specialist")') + .setStyle(TextInputStyle.Short) + .setRequired(false) + .setValue(existingSignature?.tagline || ''); + + const valedictionRow = new ActionRowBuilder().addComponents(valedictionInput); + const displayNameRow = new ActionRowBuilder().addComponents(displayNameInput); + const taglineRow = new ActionRowBuilder().addComponents(taglineInput); + + modal.addComponents(valedictionRow, displayNameRow, taglineRow); + + await interaction.showModal(modal); + } catch (err) { + console.error('Signature command error:', err); + await interaction.editReply({ content: 'Failed to open signature settings.', ephemeral: true }); + } + return; + } + // /accountinfo if (interaction.commandName === 'accountinfo') { await handleAccountInfoCommand(interaction); diff --git a/models.js b/models.js index 618e860..345ccfb 100644 --- a/models.js +++ b/models.js @@ -860,9 +860,27 @@ mongoose.model('StaffSettings', new mongoose.Schema({ })); mongoose.model('StaffNotification', new mongoose.Schema({ + userId: { type: String, required: true, unique: true }, + guildId: String, + channelId: String, + cooldownHours: { type: Number, default: 1 }, + updatedAt: { type: Date, default: Date.now } +})); + +mongoose.model('StaffSignature', new mongoose.Schema({ + userId: { type: String, required: true, unique: true }, + guildId: { type: String, required: true }, + valediction: { type: String, default: '' }, + displayName: { type: String, default: '' }, + tagline: { type: String, default: '' }, + updatedAt: { type: Date, default: Date.now } +})); + +mongoose.model('StaffSignature', new mongoose.Schema({ userId: { type: String, required: true, unique: true }, - guildId: String, - channelId: String, - cooldownHours: { type: Number, default: 1 }, + guildId: { type: String, required: true }, + valediction: { type: String, default: '' }, + displayName: { type: String, default: '' }, + tagline: { type: String, default: '' }, updatedAt: { type: Date, default: Date.now } })); diff --git a/services/gmail.js b/services/gmail.js index af0962b..69ed16a 100644 --- a/services/gmail.js +++ b/services/gmail.js @@ -157,14 +157,63 @@ async function sendTicketClosedEmail(ticket, discordDisplayName) { } } +const { mongoose } = require('../db-connection'); +const StaffSignature = mongoose.model('StaffSignature'); + +/** + * Get formatted signature blocks (text and HTML) for a staff member + * @param {string} userId - Discord user ID + * @returns {Promise<{text: string, html: string}>} Signature blocks + */ +async function getStaffSignatureBlocks(userId) { + try { + const signature = await StaffSignature.findOne({ userId }).lean(); + + if (!signature) { + return { + text: '', + html: '' + }; + } + + const valediction = signature.valediction || ''; + const displayName = signature.displayName || ''; + const tagline = signature.tagline || ''; + + const textSignature = [ + valediction, + displayName, + tagline + ].filter(Boolean).join('\n'); + + const htmlSignature = [ + valediction ? `

${escapeHtml(valediction)}

` : '', + displayName ? `

${escapeHtml(displayName)}

` : '', + tagline ? `
${escapeHtml(tagline)}
` : '' + ].filter(Boolean).join('\n'); + + return { + text: textSignature, + html: htmlSignature + }; + } catch (err) { + console.error('Error fetching staff signature:', err); + return { + text: '', + html: '' + }; + } +} + /** * Send a notification email in the ticket thread (e.g. escalation, high-priority). * @param {Object} ticket - Ticket with gmailThreadId, senderEmail, subject * @param {string} subjectLine - Subject line (e.g. "Ticket escalated" or "Priority updated") * @param {string} messageBody - Plain or HTML message body * @param {string} [fromLabel] - Label for "From" (e.g. "Support on Discord") + * @param {string} [userId] - Discord user ID for signature (optional) */ -async function sendTicketNotificationEmail(ticket, subjectLine, messageBody, fromLabel) { +async function sendTicketNotificationEmail(ticket, subjectLine, messageBody, fromLabel, userId = null) { try { const gmail = getGmailClient(); const recipientEmail = extractRawEmail(ticket.senderEmail || '').toLowerCase(); @@ -191,6 +240,13 @@ async function sendTicketNotificationEmail(ticket, subjectLine, messageBody, fro const label = escapeHtml(fromLabel || CONFIG.SUPPORT_NAME || 'Support'); const safeBody = escapeHtml(messageBody || '').replace(/\n/g, '
'); const safeLogoUrl = escapeHtml(CONFIG.LOGO_URL || ''); + + // Get staff signature if userId provided + let signatureBlocks = { text: '', html: '' }; + if (userId) { + signatureBlocks = await getStaffSignatureBlocks(userId); + } + const safeSignature = escapeHtml(CONFIG.SIGNATURE || ''); const htmlBody = `
@@ -203,8 +259,9 @@ async function sendTicketNotificationEmail(ticket, subjectLine, messageBody, fro ${safeLogoUrl ? `` : ''} -

${label}

-
${safeSignature}
+ ${signatureBlocks.html || `

${label}

`} + ${signatureBlocks.html ? '
' : ''} + ${signatureBlocks.html ? signatureBlocks.html : `
${safeSignature}
`} @@ -237,6 +294,84 @@ async function sendTicketNotificationEmail(ticket, subjectLine, messageBody, fro } } +/** + * Send a Gmail reply to a ticket + * @param {string} threadId - Gmail thread ID + * @param {string} replyText - Reply text + * @param {string} recipientEmail - Recipient email + * @param {string} subject - Subject line + * @param {string} discordUser - Discord user name + * @param {string} messageId - Message ID (optional) + * @param {string} userId - Discord user ID for signature (optional) + */ +async function sendGmailReply( + threadId, + replyText, + recipientEmail, + subject, + discordUser, + messageId, + userId = null +) { + const gmail = getGmailClient(); + + const utf8Subject = `=?utf-8?B?${Buffer.from( + `Re: ${subject}` + ).toString('base64')}?=`; + const safeUser = escapeHtml(discordUser); + const safeReply = escapeHtml(replyText).replace(/\n/g, '
'); + const safeLogoUrl = escapeHtml(CONFIG.LOGO_URL || ''); + const safeSignature = escapeHtml(CONFIG.SIGNATURE || ''); + + // Get staff signature if userId provided + let signatureBlocks = { text: '', html: '' }; + if (userId) { + signatureBlocks = await getStaffSignatureBlocks(userId); + } + + const htmlBody = ` +
+

From: ${safeUser} on Discord

+

${safeReply}

+
+ + + + + +
+ ${safeLogoUrl ? `` : ''} + + ${signatureBlocks.html || `

${safeUser}

`} + ${signatureBlocks.html ? '
' : ''} + ${signatureBlocks.html ? signatureBlocks.html : `
${safeSignature}
`} +
+
`; + + const headers = [ + `From: ${CONFIG.MY_EMAIL}`, + `To: ${recipientEmail}`, + `Subject: ${utf8Subject}`, + messageId ? `In-Reply-To: ${messageId}` : '', + messageId ? `References: ${messageId}` : '', + 'MIME-Version: 1.0', + 'Content-Type: text/html; charset="UTF-8"', + '', + htmlBody + ].filter(Boolean); + + const raw = Buffer.from(headers.join('\r\n')) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + + await gmail.users.messages.send({ + userId: 'me', + requestBody: { raw, threadId } + }); +} + module.exports = { getGmailClient, sendGmailReply, diff --git a/services/staffSignature.js b/services/staffSignature.js new file mode 100644 index 0000000..fb972f0 --- /dev/null +++ b/services/staffSignature.js @@ -0,0 +1,25 @@ +const { mongoose } = require('../db-connection'); + +/** + * Returns { text, html } for a staff member's signature. + * Returns { text: '', html: '' } if no signature is set. + */ +async function getStaffSignatureBlocks(userId) { + const StaffSignature = mongoose.model('StaffSignature'); + const sig = await StaffSignature.findOne({ userId }).lean(); + if (!sig || (!sig.valediction && !sig.displayName && !sig.tagline)) { + return { text: '', html: '' }; + } + + const lines = []; + if (sig.valediction) lines.push(sig.valediction); + if (sig.displayName) lines.push(sig.displayName); + if (sig.tagline) lines.push(sig.tagline); + + const text = lines.join('\n'); + const html = lines.map(l => `
${l}
`).join(''); + + return { text, html }; +} + +module.exports = { getStaffSignatureBlocks }; \ No newline at end of file