[SEC-004] services/staffThread.js — addRoleMembersToThread previously called
the unscoped guild.members.fetch() on every ticket creation, chunking every
member of the guild. With STAFF_THREAD_AUTO_ADD_ROLE on and a 50-member
staff role, the 300ms-per-add loop also blocked ticket creation for ~15s.
- Read role.members directly (computed from guild.members.cache, kept in
sync via the GuildMembers gateway intent set on the client). Skip the
explicit unscoped fetch in the hot path.
- Cache-cold fallback: one scoped guild.members.fetch({ withPresences:
false }) — irrelevant presence sync stays off the wire.
- createStaffThread no longer awaits the add-loop. Wraps the call in
setImmediate(...) so ticket creation returns immediately while the
rate-limited add-loop runs in the background.
[SEC-005] services/debugLog.js — stacks/messages posted to the debug
channel could leak customer email addresses (interpolated through ticket
errors) and Discord member/channel IDs. Add a redactPII helper applied to
both logError's message + stack and logWarn's body:
- Email regex /[\w.+-]+@[\w.-]+\.\w+/g → [EMAIL_REDACTED]
- Discord snowflake /\b\d{18,20}\b/g → [ID_REDACTED]
interaction.user.tag in the User: line is intentionally not redacted —
it's needed for triage and is not PII (Discord usernames are public).
100 lines
3.1 KiB
JavaScript
100 lines
3.1 KiB
JavaScript
/**
|
||
* Structured logging service – posts embeds to dedicated Discord channels.
|
||
* Call setClient(client) from the main bot on ready so logs can be posted.
|
||
*/
|
||
const { EmbedBuilder } = require('discord.js');
|
||
const { CONFIG } = require('../config');
|
||
|
||
let client = null;
|
||
|
||
function setClient(c) {
|
||
client = c;
|
||
}
|
||
|
||
// --- PII redaction ---
|
||
|
||
// Email addresses (loose regex — covers most RFC 5321 local parts that show up
|
||
// in support traffic) and Discord snowflakes (18–20 digit numeric IDs) get
|
||
// redacted before stack/message text reaches the debug channel. Both can land
|
||
// in error stacks via senderEmail interpolation, channel IDs in error
|
||
// messages, etc. — redacting at the boundary keeps the debug channel useful
|
||
// for triage without leaking customer addresses or staff member IDs.
|
||
const EMAIL_REDACT_RE = /[\w.+-]+@[\w.-]+\.\w+/g;
|
||
const SNOWFLAKE_REDACT_RE = /\b\d{18,20}\b/g;
|
||
|
||
function redactPII(s) {
|
||
if (s == null) return '';
|
||
return String(s)
|
||
.replace(EMAIL_REDACT_RE, '[EMAIL_REDACTED]')
|
||
.replace(SNOWFLAKE_REDACT_RE, '[ID_REDACTED]');
|
||
}
|
||
|
||
// --- Helpers ---
|
||
|
||
async function sendToChannel(channelId, embed, overrideClient) {
|
||
const c = overrideClient || client;
|
||
if (!c || !channelId) return;
|
||
try {
|
||
const channel = await c.channels.fetch(channelId);
|
||
if (channel) await channel.send({ embeds: [embed] });
|
||
} catch (_) {
|
||
// ignore send failures
|
||
}
|
||
}
|
||
|
||
// --- logError (backwards-compatible) ---
|
||
|
||
async function logError(context, error, interaction = null, overrideClient = null) {
|
||
const c = overrideClient || client;
|
||
if (!c || !CONFIG.DEBUGGING_CHANNEL_ID) return;
|
||
|
||
try {
|
||
const channel = await c.channels.fetch(CONFIG.DEBUGGING_CHANNEL_ID);
|
||
const userLine = interaction?.user?.tag
|
||
? `User: ${interaction.user.tag}\n`
|
||
: '';
|
||
const commandLine = (interaction?.commandName || interaction?.customId)
|
||
? `Command/Button: ${interaction.commandName || interaction.customId}\n`
|
||
: '';
|
||
const message = redactPII(error.message || String(error));
|
||
const stack = redactPII(error.stack || error.message || String(error)).slice(0, 1500);
|
||
await channel.send({
|
||
content: `\`[${context}]\` ${message}\n${userLine}${commandLine}\n\`\`\`${stack}\`\`\``
|
||
});
|
||
} catch (_) {
|
||
// ignore send failures
|
||
}
|
||
}
|
||
|
||
// --- logWarn ---
|
||
|
||
async function logWarn(context, message, overrideClient = null) {
|
||
const embed = new EmbedBuilder()
|
||
.setTitle(`Warning: ${context}`)
|
||
.setDescription(redactPII(String(message)).slice(0, 4000))
|
||
.setColor(0xFFFF00)
|
||
.setTimestamp();
|
||
await sendToChannel(CONFIG.DEBUGGING_CHANNEL_ID, embed, overrideClient);
|
||
}
|
||
|
||
// --- logTicketEvent ---
|
||
|
||
async function logTicketEvent(action, fields, interaction = null) {
|
||
const embed = new EmbedBuilder()
|
||
.setTitle(action)
|
||
.setColor(CONFIG.EMBED_COLOR_INFO || 0x1e2124)
|
||
.addFields(fields.map(f => ({ name: f.name, value: String(f.value).slice(0, 1024), inline: f.inline ?? true })))
|
||
.setTimestamp();
|
||
if (interaction?.user?.tag) {
|
||
embed.setFooter({ text: interaction.user.tag });
|
||
}
|
||
await sendToChannel(CONFIG.LOGGING_CHANNEL_ID, embed, interaction?.client);
|
||
}
|
||
|
||
module.exports = {
|
||
setClient,
|
||
logError,
|
||
logWarn,
|
||
logTicketEvent
|
||
};
|