Inbound: - Gmail poll query is:unread in:inbox (was category:primary, which matched nothing on a no-tabs Workspace inbox) Outbound email: - Closed/escalation auto-emails editable via TICKET_CLOSE_MESSAGE and new TICKET_ESCALATION_EMAIL_MESSAGE; drop the staff signature from closing emails - Replies quote the customer's latest message (gmail_quote markup so clients collapse it), embed custom emoji inline via CID attachment, and strip Discord role mentions - Tagline spacing fix in the company signature Discord side: - Suppress all mentions in log + transcript posts (no more pinging on close) - Drop the staff-role ping from new-ticket and follow-up notifications - Ticket channels inherit category permissions instead of setting per-channel overwrites (removes the Manage Roles requirement) Gmail folders: - Folder/label routing (gmailLabels.js) with /folder; close files to Complete Config: - Remove ~56 stale .env keys for long-removed features; refresh stale copy Docs: - Design specs for folder routing, email-flow toggle, and per-staff metrics
101 lines
3.2 KiB
JavaScript
101 lines
3.2 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], allowedMentions: { parse: [] } });
|
||
} 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}\`\`\``,
|
||
allowedMentions: { parse: [] }
|
||
});
|
||
} 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
|
||
};
|