audit
This commit is contained in:
100
utils.js
100
utils.js
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user