This commit is contained in:
2026-04-20 18:05:36 +00:00
parent d73422555d
commit 33b1f276c6
26 changed files with 598 additions and 183 deletions

100
utils.js
View File

@@ -2,8 +2,24 @@
* Pure utility functions text processing, date formatting, game detection,
* priority helpers, template variables.
*/
const crypto = require('crypto');
const { CONFIG, GAME_NAMES, GAME_ALIASES, TICKET_TAGS } = require('./config');
/** Constant-time string compare. Returns false for mismatched length or empty/nullish inputs without throwing. */
function safeEqual(a, b) {
const ab = Buffer.from(String(a || ''), 'utf8');
const bb = Buffer.from(String(b || ''), 'utf8');
return ab.length === bb.length && crypto.timingSafeEqual(ab, bb);
}
/** True if the member holds ROLE_ID_TO_PING or any ADDITIONAL_STAFF_ROLES. Safe for null/undefined members. */
function isStaff(member) {
if (!member?.roles?.cache) return false;
if (CONFIG.ROLE_ID_TO_PING && member.roles.cache.has(CONFIG.ROLE_ID_TO_PING)) return true;
const additional = CONFIG.ADDITIONAL_STAFF_ROLES || [];
return additional.some(roleId => member.roles.cache.has(roleId));
}
// --- TEXT PROCESSING ---
const BLOCK_TAG_REGEX =
@@ -116,40 +132,37 @@ function stripEmailQuotes(text) {
return cleaned.trim();
}
// Hoisted to module scope: constructed once at load, not per-call.
const MOBILE_FOOTER_REGEXES = [
/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
].map(re => new RegExp(`\\n*${re.source}\\s*`, 'i'));
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');
for (const rx of MOBILE_FOOTER_REGEXES) {
result = result.replace(rx, '');
}
return result;
}
@@ -186,22 +199,25 @@ const getFormattedDate = () => {
};
// --- GAME DETECTION ---
// Map<lowercase-alias, { canonical, re }> built once at module load so detectGame
// doesn't allocate a fresh RegExp per game/alias per call.
const GAME_DETECTION = (() => {
const m = new Map();
const add = (key, canonical) => {
const lower = String(key).toLowerCase();
if (m.has(lower)) return;
m.set(lower, { canonical, re: new RegExp(`\\b${escapeRegex(lower)}\\b`, 'i') });
};
for (const game of GAME_NAMES) add(game, game);
for (const [alias, fullName] of Object.entries(GAME_ALIASES)) add(alias, fullName);
return m;
})();
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 { re, canonical } of GAME_DETECTION.values()) {
if (re.test(txt)) return canonical;
}
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';
};
@@ -378,6 +394,8 @@ module.exports = {
BLOCK_TAG_REGEX,
escapeRegex,
escapeHtml,
safeEqual,
isStaff,
decodeHtmlEntities,
htmlToTextWithBlocks,
decodeGmailData,