283
utils.js
Normal file
283
utils.js
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* 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(/<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,
|
||||
decodeHtmlEntities,
|
||||
htmlToTextWithBlocks,
|
||||
decodeGmailData,
|
||||
getCleanBody,
|
||||
stripEmailQuotes,
|
||||
stripMobileFooter,
|
||||
extractRawEmail,
|
||||
getFormattedDate,
|
||||
detectGame,
|
||||
getPriorityEmoji,
|
||||
getPriorityColor,
|
||||
getTicketTagEmoji,
|
||||
replaceVariables
|
||||
};
|
||||
Reference in New Issue
Block a user