296 lines
7.8 KiB
JavaScript
296 lines
7.8 KiB
JavaScript
/**
|
||
* 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, '\\$&');
|
||
}
|
||
|
||
/** Escape for safe use in HTML body (prevents XSS in outgoing emails). */
|
||
function escapeHtml(str) {
|
||
if (str == null) return '';
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.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(/<br\s*\/?>/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,
|
||
escapeHtml,
|
||
decodeHtmlEntities,
|
||
htmlToTextWithBlocks,
|
||
decodeGmailData,
|
||
getCleanBody,
|
||
stripEmailQuotes,
|
||
stripMobileFooter,
|
||
extractRawEmail,
|
||
getFormattedDate,
|
||
detectGame,
|
||
getPriorityEmoji,
|
||
getPriorityColor,
|
||
getTicketTagEmoji,
|
||
replaceVariables
|
||
};
|