/** * Pure utility functions – text processing, date formatting, game detection, * priority helpers, template variables. */ const { CONFIG, GAME_NAMES, GAME_ALIASES, TICKET_TAGS } = require('./config'); // --- TEXT PROCESSING --- const BLOCK_TAG_REGEX = /<\/(p|div|li|h[1-6]|tr|table|section|article|blockquote)>/gi; function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function decodeHtmlEntities(str) { if (!str) return ''; return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/ /g, ' '); } function htmlToTextWithBlocks(html) { return decodeHtmlEntities( html .replace(/\r\n/g, '\n') .replace(//gi, '\n') .replace(BLOCK_TAG_REGEX, '\n\n') .replace(/<(ul|ol)[^>]*>/gi, '\n') .replace(/<[^>]*>?/gm, '') ); } // --- EMAIL BODY EXTRACTION --- function decodeGmailData(p) { if (!p.body?.data) return ''; let data = Buffer.from(p.body.data, 'base64').toString('utf8'); const isQuotedPrintable = p.headers?.some( h => h.name.toLowerCase() === 'content-transfer-encoding' && h.value.toLowerCase() === 'quoted-printable' ); if (isQuotedPrintable) { data = data .replace(/=\r?\n/g, '') .replace(/=([0-9A-F]{2})/gi, (m, hex) => String.fromCharCode(parseInt(hex, 16)) ); } return data; } function getCleanBody(payload) { let body = ''; const findParts = parts => { for (const part of parts) { if (part.mimeType === 'text/plain' && part.body?.data && !body) { body = decodeGmailData(part); } if (part.mimeType === 'text/html' && part.body?.data && !body) { body = decodeGmailData(part); } if (part.parts) findParts(part.parts); } }; if (payload.parts) { findParts(payload.parts); } else if (payload.body?.data) { body = decodeGmailData(payload); } return body || payload.snippet || ''; } // --- QUOTE / FOOTER STRIPPING --- function stripEmailQuotes(text) { let cleaned = text.replace(/\r\n/g, '\n'); const markers = [ /\n_{5,}\s*$/m, /\nFrom:\s.*<.*@.*>/i, /\nSent:\s.*$/i, /\nTo:\s.*$/i, /\nSubject:\s.*$/i, /\nOn .* wrote:/i ]; for (const m of markers) { const match = cleaned.match(m); if (match) { cleaned = cleaned.substring(0, match.index); break; } } return cleaned.trim(); } function stripMobileFooter(text) { if (!text) return text; const patterns = [ /Sent from my iPhone/i, /Sent from my iPad/i, /Sent from my Apple Watch/i, /Sent from my Mac/i, /Sent from my mobile device/i, /Sent from my phone/i, /Sent from my smartphone/i, /Sent from my Android(?: phone| device)?/i, /Sent from my Samsung Galaxy smartphone/i, /Sent from Samsung Mobile/i, /Sent from my Galaxy/i, /Sent from my BlackBerry/i, /Sent from my Windows Phone/i, /Sent from Outlook for iOS/i, /Sent from Outlook for Android/i, /Sent from Yahoo Mail for iPhone(?: \/ Android)?/i, /Sent from Yahoo Mail for Android/i, /Sent from my Amazon Fire/i, /Get\s+Outlook\s+for\s+iOS/i, /Get\s+Outlook\s+for\s+Android/i, /Sent with Proton Mail secure email\./i ]; let result = text; for (const re of patterns) { const rx = new RegExp(`\\n*${re.source}\\s*`, 'i'); result = result.replace(rx, ''); } return result; } // --- EMAIL HELPERS --- function extractRawEmail(headerValue) { const match = headerValue.match(/<([^>]+)>/); return match ? match[1].trim() : headerValue.trim(); } // --- DATE --- const getFormattedDate = () => { const now = new Date(); const datePart = now .toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' }) .replace(/\//g, '-'); const timePart = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true }); const tzPart = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }) .formatToParts(now) .find(p => p.type === 'timeZoneName').value; return `${datePart} ${timePart} ${tzPart}`; }; // --- GAME DETECTION --- const detectGame = (subject, body) => { const txt = `${subject} ${body}`.toLowerCase(); for (const game of GAME_NAMES) { const g = game.toLowerCase(); const re = new RegExp(`\\b${escapeRegex(g)}\\b`, 'i'); if (re.test(txt)) return game; } for (const [alias, fullName] of Object.entries(GAME_ALIASES)) { const a = alias.toLowerCase(); const re = new RegExp(`\\b${escapeRegex(a)}\\b`, 'i'); if (re.test(txt)) return fullName; } return 'Not Mentioned'; }; // --- PRIORITY --- function getPriorityEmoji(priority) { switch (priority) { case 'high': return CONFIG.PRIORITY_HIGH_EMOJI; case 'low': return CONFIG.PRIORITY_LOW_EMOJI; case 'normal': case 'medium': default: return CONFIG.PRIORITY_MEDIUM_EMOJI; } } function getPriorityColor(priority) { switch (priority) { case 'high': return 0xFF0000; case 'low': return 0x00FF00; case 'normal': case 'medium': default: return CONFIG.EMBED_COLOR_INFO; } } /** Returns emoji for a ticket-tag key (e.g. server-down → ⬇️). Priority always comes first in channel name, then tag. */ function getTicketTagEmoji(tagKey) { if (!tagKey) return ''; const t = (TICKET_TAGS || []).find(x => x.value === tagKey); return t ? t.emoji : ''; } // --- TEMPLATE VARIABLES --- function replaceVariables(template, context = {}) { if (!template) return ''; let result = template; if (context.ticket) { result = result.replace(/{ticket\.user}/g, context.ticket.sender_name || 'Unknown'); result = result.replace(/{ticket\.creator}/g, context.ticket.sender_name || 'Unknown'); result = result.replace(/{ticket\.email}/g, context.ticket.senderEmail || ''); result = result.replace(/{ticket\.number}/g, context.ticket.ticketNumber != null ? context.ticket.ticketNumber : 'N/A'); result = result.replace(/{ticket\.subject}/g, context.ticket.subject || 'No subject'); result = result.replace(/{ticket\.claimed}/g, context.ticket.claimedBy ? 'Yes' : 'No'); result = result.replace(/{ticket\.claimedby}/g, context.ticket.claimedBy || 'Unclaimed'); result = result.replace(/{ticket\.priority}/g, context.ticket.priority || 'normal'); result = result.replace(/{ticket\.id}/g, context.ticket.gmailThreadId || ''); } if (context.staff) { result = result.replace(/{staff\.user}/g, context.staff.username || ''); result = result.replace(/{staff\.name}/g, context.staff.displayName || context.staff.username || ''); result = result.replace(/{staff\.mention}/g, context.staff.mention || ''); } if (context.guild) { result = result.replace(/{server\.name}/g, context.guild.name || ''); result = result.replace(/{server\.membercount}/g, context.guild.memberCount?.toString() || '0'); } if (context.hours !== undefined) { result = result.replace(/{hours}/g, context.hours.toString()); } const now = new Date(); result = result.replace(/{date}/g, now.toLocaleDateString()); result = result.replace(/{time}/g, now.toLocaleTimeString()); return result; } module.exports = { BLOCK_TAG_REGEX, escapeRegex, decodeHtmlEntities, htmlToTextWithBlocks, decodeGmailData, getCleanBody, stripEmailQuotes, stripMobileFooter, extractRawEmail, getFormattedDate, detectGame, getPriorityEmoji, getPriorityColor, getTicketTagEmoji, replaceVariables };