Files
broccolini-bot/utils.js

296 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function decodeHtmlEntities(str) {
if (!str) return '';
return str
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&nbsp;/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
};