manual commit 2026-04-10T19:27:53Z
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
24
models.js
24
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 }
|
||||
}));
|
||||
|
||||
@@ -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 ? `<p style="margin: 0;">${escapeHtml(valediction)}</p>` : '',
|
||||
displayName ? `<p style="margin: 0; font-weight: bold;">${escapeHtml(displayName)}</p>` : '',
|
||||
tagline ? `<div style="color: #666; font-size: 12px;">${escapeHtml(tagline)}</div>` : ''
|
||||
].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, '<br>');
|
||||
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 = `
|
||||
<div style="font-family: sans-serif; font-size: 14px; color: #333;">
|
||||
@@ -203,8 +259,9 @@ async function sendTicketNotificationEmail(ticket, subjectLine, messageBody, fro
|
||||
${safeLogoUrl ? `<img src="${safeLogoUrl}" width="65">` : ''}
|
||||
</td>
|
||||
<td style="border-left: 1px solid #ddd; padding-left: 12px;">
|
||||
<p style="margin: 0; font-weight: bold;">${label}</p>
|
||||
<div style="color: #666; font-size: 12px;">${safeSignature}</div>
|
||||
${signatureBlocks.html || `<p style="margin: 0; font-weight: bold;">${label}</p>`}
|
||||
${signatureBlocks.html ? '<hr style="border:none; border-top:1px solid #ddd; margin:10px 0;">' : ''}
|
||||
${signatureBlocks.html ? signatureBlocks.html : `<div style="color: #666; font-size: 12px;">${safeSignature}</div>`}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -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, '<br>');
|
||||
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 = `
|
||||
<div style="font-family: sans-serif; font-size: 14px; color: #333;">
|
||||
<p><strong>From:</strong> ${safeUser} on Discord</p>
|
||||
<p>${safeReply}</p>
|
||||
<hr style="border:none; border-top:1px solid #ddd; margin:20px 0;">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td style="padding-right: 12px;">
|
||||
${safeLogoUrl ? `<img src="${safeLogoUrl}" width="65">` : ''}
|
||||
</td>
|
||||
<td style="border-left: 1px solid #ddd; padding-left: 12px;">
|
||||
${signatureBlocks.html || `<p style="margin: 0; font-weight: bold;">${safeUser}</p>`}
|
||||
${signatureBlocks.html ? '<hr style="border:none; border-top:1px solid #ddd; margin:10px 0;">' : ''}
|
||||
${signatureBlocks.html ? signatureBlocks.html : `<div style="color: #666; font-size: 12px;">${safeSignature}</div>`}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>`;
|
||||
|
||||
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,
|
||||
|
||||
25
services/staffSignature.js
Normal file
25
services/staffSignature.js
Normal file
@@ -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 => `<div>${l}</div>`).join('');
|
||||
|
||||
return { text, html };
|
||||
}
|
||||
|
||||
module.exports = { getStaffSignatureBlocks };
|
||||
Reference in New Issue
Block a user