Event-sourced tracking of staff/ticket lifecycle actions, plus a /stats command. Foundation for a future tickets-website analytics dashboard. Data: - StaffAction model (event log) + Ticket.game / Ticket.closedAt - STATS_ADMIN_IDS config (who may view others' stats) Recording (fire-and-forget, idempotent on real state transitions): - claim, response (channel reply + /response send), escalate, de-escalate, transfer, close (4 sites), reopen — each denormalizes ticketType, tier, priority, game, requester (senderEmail / creatorId), guildId - close events carry closerType / resolverId (claimer credit) / wasClaimed; transfer carries fromId / toId; reopen stamps resolverId - conditional close transition helper (atomic open->closed + closedAt) shared by all four close paths Query + command: - pure period parser (presets + free-text) and stats shaper (per-metric keys) - command-aware autocomplete dispatch - /stats: period (autocomplete) + member (admin-gated) + source (all/email/ discord), ManageMessages + staff-role gated, ephemeral, tier-labeled embed 288+ unit tests; timing/busiest-times data is collected but displayed later.
124 lines
7.9 KiB
JavaScript
124 lines
7.9 KiB
JavaScript
/**
|
|
* Broccolini Bot configuration and game lists.
|
|
*
|
|
* Never commit .env; agents must not modify .env without explicit user confirmation.
|
|
*/
|
|
require('dotenv').config({ debug: process.env.NODE_ENV === 'development' });
|
|
|
|
function toInt(v, fallback) {
|
|
const n = parseInt(v, 10);
|
|
return Number.isFinite(n) ? n : fallback;
|
|
}
|
|
|
|
const CONFIG = {
|
|
DISCORD_TOKEN: (process.env.DISCORD_TOKEN || process.env.DISCORD_BOT_TOKEN || '').trim(),
|
|
DISCORD_GUILD_ID: process.env.DISCORD_GUILD_ID || null,
|
|
TICKET_CATEGORY_ID: process.env.TICKET_CATEGORY_ID,
|
|
TICKET_CATEGORY_NAME: process.env.TICKET_CATEGORY_NAME || 'Open Tickets',
|
|
DISCORD_TICKET_CATEGORY_ID: process.env.DISCORD_TICKET_CATEGORY_ID || process.env.TICKET_CATEGORY_ID,
|
|
ROLE_ID_TO_PING: process.env.ROLE_ID_TO_PING,
|
|
TRANSCRIPT_CHANNEL_ID: process.env.TRANSCRIPT_CHANNEL_ID,
|
|
LOGGING_CHANNEL_ID: process.env.LOGGING_CHANNEL_ID,
|
|
DEBUGGING_CHANNEL_ID: process.env.DEBUGGING_CHANNEL_ID || null,
|
|
CLIENT_ID: process.env.DISCORD_APPLICATION_ID,
|
|
REFRESH_TOKEN: process.env.REFRESH_TOKEN,
|
|
MY_EMAIL: (process.env.MY_EMAIL || '').toLowerCase(),
|
|
LOGO_URL: process.env.LOGO_URL,
|
|
SUPPORT_NAME: process.env.SUPPORT_NAME || 'Support',
|
|
STAFF_EMOJIS: Object.fromEntries((process.env.STAFF_EMOJIS||'').split(',').map(s=>s.trim()).filter(Boolean).map(p=>{const i=p.indexOf(':');return i===-1?null:[p.slice(0,i).trim(),p.slice(i+1).trim()];}).filter(Boolean)),
|
|
CLAIMER_EMOJI_FALLBACK: process.env.CLAIMER_EMOJI_FALLBACK || '🎫',
|
|
PORT: toInt(process.env.DISCORD_ONLY_PORT, 5000),
|
|
HEALTHCHECK_HOST: process.env.HEALTHCHECK_HOST || null, // null = listen on all interfaces; set to 127.0.0.1 for local-only
|
|
GAME_LIST: process.env.GAME_LIST || '',
|
|
// Tier 2/3 escalation: category IDs where ticket channels are placed (env uses *_CHANNEL_* for legacy naming).
|
|
EMAIL_ESCALATED2_CHANNEL_ID: process.env.EMAIL_ESCALATED2_CHANNEL_ID || null,
|
|
DISCORD_ESCALATED2_CHANNEL_ID: process.env.DISCORD_ESCALATED2_CHANNEL_ID || null,
|
|
EMAIL_ESCALATED3_CHANNEL_ID: process.env.EMAIL_ESCALATED3_CHANNEL_ID || null,
|
|
DISCORD_ESCALATED3_CHANNEL_ID: process.env.DISCORD_ESCALATED3_CHANNEL_ID || null,
|
|
ESCALATION_MESSAGE: process.env.ESCALATION_MESSAGE || 'Your ticket has been escalated.\n\nA senior {support_name} will be here to assist as soon as possible.',
|
|
// Email tickets only (escalation notification email body). Placeholders: {escalator_name}, {tier}.
|
|
TICKET_ESCALATION_EMAIL_MESSAGE: process.env.TICKET_ESCALATION_EMAIL_MESSAGE || '{escalator_name} escalated this ticket to {tier}.',
|
|
TICKET_CLOSE_SUBJECT_PREFIX: process.env.TICKET_CLOSE_SUBJECT_PREFIX || '[Resolved]',
|
|
// Email tickets only (closure email body):
|
|
TICKET_CLOSE_MESSAGE: process.env.TICKET_CLOSE_MESSAGE || '{closer_name} has marked this ticket as resolved. If you would like to re-open this issue, please reply to this email.',
|
|
TICKET_CLOSE_SIGNATURE: process.env.TICKET_CLOSE_SIGNATURE || 'Thank you for using Indifferent Broccoli.',
|
|
// Discord ticket closure (in-channel and transcript):
|
|
DISCORD_CLOSE_MESSAGE: process.env.DISCORD_CLOSE_MESSAGE || 'This ticket has been closed. A transcript has been saved. If you still need assistance, please open a new ticket.',
|
|
DISCORD_TRANSCRIPT_MESSAGE: process.env.DISCORD_TRANSCRIPT_MESSAGE || 'Your ticket **{channel_name}** has been closed. Here is your transcript. If you still need assistance, please open a new ticket.',
|
|
DISCORD_AUTO_CLOSE_MESSAGE: process.env.DISCORD_AUTO_CLOSE_MESSAGE || 'This ticket was closed due to inactivity. If you still need assistance, please open a new ticket.',
|
|
AUTO_CLOSE_ENABLED: process.env.AUTO_CLOSE_ENABLED === 'true',
|
|
AUTO_CLOSE_AFTER_HOURS: toInt(process.env.AUTO_CLOSE_AFTER_HOURS, 72),
|
|
GLOBAL_TICKET_LIMIT: toInt(process.env.GLOBAL_TICKET_LIMIT, 5),
|
|
RATE_LIMIT_TICKETS_PER_USER: toInt(process.env.RATE_LIMIT_TICKETS_PER_USER, 0),
|
|
RATE_LIMIT_WINDOW_MINUTES: toInt(process.env.RATE_LIMIT_WINDOW_MINUTES, 60),
|
|
BLACKLISTED_ROLES: (process.env.BLACKLISTED_ROLES || '').split(',').map(r => r.trim()).filter(Boolean),
|
|
ADDITIONAL_STAFF_ROLES: (process.env.ADDITIONAL_STAFF_ROLES || '').split(',').map(r => r.trim()).filter(Boolean),
|
|
STATS_ADMIN_IDS: (process.env.STATS_ADMIN_IDS || '').split(',').map(r => r.trim()).filter(Boolean),
|
|
TICKET_WELCOME_MESSAGE: process.env.TICKET_WELCOME_MESSAGE || "We got your ticket. We'll be with you as soon as possible. Feel free to add any additional information to your ticket.",
|
|
TICKET_CLAIMED_MESSAGE: process.env.TICKET_CLAIMED_MESSAGE || 'Ticket claimed by {staff_mention} 🚀',
|
|
TICKET_UNCLAIMED_MESSAGE: process.env.TICKET_UNCLAIMED_MESSAGE || 'Ticket unclaimed by {staff_mention} ☀️',
|
|
PRIORITY_ENABLED: process.env.PRIORITY_ENABLED === 'true',
|
|
DEFAULT_PRIORITY: process.env.DEFAULT_PRIORITY || 'normal',
|
|
PRIORITY_HIGH_EMOJI: process.env.PRIORITY_HIGH_EMOJI || '🔴',
|
|
PRIORITY_MEDIUM_EMOJI: process.env.PRIORITY_MEDIUM_EMOJI || '🟡',
|
|
PRIORITY_LOW_EMOJI: process.env.PRIORITY_LOW_EMOJI || '🟢',
|
|
AUTO_UNCLAIM_ENABLED: process.env.AUTO_UNCLAIM_ENABLED === 'true',
|
|
AUTO_UNCLAIM_AFTER_HOURS: toInt(process.env.AUTO_UNCLAIM_AFTER_HOURS, 24),
|
|
ALLOW_CLAIM_OVERWRITE: process.env.ALLOW_CLAIM_OVERWRITE === 'true',
|
|
BUTTON_LABEL_CLOSE: process.env.BUTTON_LABEL_CLOSE || 'Close Ticket',
|
|
BUTTON_LABEL_CLAIM: process.env.BUTTON_LABEL_CLAIM || 'Claim',
|
|
BUTTON_LABEL_UNCLAIM: process.env.BUTTON_LABEL_UNCLAIM || 'Unclaim',
|
|
BUTTON_EMOJI_CLOSE: process.env.BUTTON_EMOJI_CLOSE || '🔒',
|
|
BUTTON_EMOJI_CLAIM: process.env.BUTTON_EMOJI_CLAIM || '📌',
|
|
BUTTON_EMOJI_UNCLAIM: process.env.BUTTON_EMOJI_UNCLAIM || '🔓',
|
|
EMBED_COLOR_OPEN: toInt(process.env.EMBED_COLOR_OPEN, 0x00FF00),
|
|
EMBED_COLOR_CLAIMED: toInt(process.env.EMBED_COLOR_CLAIMED, 0xFFFF00),
|
|
EMBED_COLOR_ESCALATED: toInt(process.env.EMBED_COLOR_ESCALATED, 0xFF6600),
|
|
EMBED_COLOR_INFO: toInt(process.env.EMBED_COLOR_INFO, 0x1e2124),
|
|
ADMIN_ID: process.env.ADMIN_ID || null,
|
|
FORCE_CLOSE_TIMER: toInt(process.env.FORCE_CLOSE_TIMER_SECONDS, 60),
|
|
GMAIL_POLL_INTERVAL_MS: toInt(process.env.GMAIL_POLL_INTERVAL_SECONDS, 30) * 1000,
|
|
// Inbound email flow master switch. Absent/anything-but-"false" => on, so
|
|
// existing deployments keep polling with no .env change. Toggle via /email.
|
|
GMAIL_POLL_ENABLED: process.env.GMAIL_POLL_ENABLED !== 'false',
|
|
// Gmail "folder" (label) names for ticket-lifecycle routing — see services/gmailLabels.js.
|
|
GMAIL_LABEL_TRIAGE: process.env.GMAIL_LABEL_TRIAGE || 'Triage',
|
|
GMAIL_LABEL_ESCALATED: process.env.GMAIL_LABEL_ESCALATED || 'Escalated',
|
|
GMAIL_LABEL_RESOLVED: process.env.GMAIL_LABEL_RESOLVED || 'Resolved',
|
|
GMAIL_LABEL_FOR_JAKE: process.env.GMAIL_LABEL_FOR_JAKE || 'For Jake',
|
|
GMAIL_LABEL_DASHBOARD_ERRORS: process.env.GMAIL_LABEL_DASHBOARD_ERRORS || 'Dashboard Errors',
|
|
GMAIL_LABEL_PARTNERSHIP_OFFERS: process.env.GMAIL_LABEL_PARTNERSHIP_OFFERS || 'Partnership Offers',
|
|
STAFF_THREAD_ENABLED: process.env.STAFF_THREAD_ENABLED === 'true',
|
|
STAFF_THREAD_NAME: process.env.STAFF_THREAD_NAME || 'Staff Discussion',
|
|
STAFF_THREAD_AUTO_ADD_ROLE: process.env.STAFF_THREAD_AUTO_ADD_ROLE === 'true',
|
|
STAFF_THREAD_ROLE_ID: process.env.STAFF_THREAD_ROLE_ID || process.env.ROLE_ID_TO_PING || null,
|
|
PIN_INITIAL_MESSAGE_ENABLED: process.env.PIN_INITIAL_MESSAGE_ENABLED === 'true',
|
|
PIN_ESCALATION_MESSAGE_ENABLED: process.env.PIN_ESCALATION_MESSAGE_ENABLED === 'true',
|
|
TRANSCRIPT_DM_TO_CREATOR: process.env.TRANSCRIPT_DM_TO_CREATOR === 'true',
|
|
PIN_SUPPRESS_SYSTEM_MESSAGE: process.env.PIN_SUPPRESS_SYSTEM_MESSAGE === 'true',
|
|
INTERNAL_API_PORT: toInt(process.env.INTERNAL_API_PORT, 12753),
|
|
INTERNAL_API_SECRET: process.env.INTERNAL_API_SECRET || null
|
|
};
|
|
|
|
const GAME_NAMES = (CONFIG.GAME_LIST || '')
|
|
.split(',')
|
|
.map(g => g.trim())
|
|
.filter(Boolean);
|
|
|
|
const GAME_ALIASES = {
|
|
'7D2D': '7 Days to Die',
|
|
'7 days': '7 Days to Die',
|
|
PZ: 'Project Zomboid',
|
|
zomboid: 'Project Zomboid',
|
|
MC: 'Minecraft',
|
|
Ark: 'ARK: Survival Evolved',
|
|
SOTF: 'Sons of the Forest',
|
|
CS2: 'Counter-Strike 2'
|
|
};
|
|
|
|
module.exports = {
|
|
CONFIG,
|
|
GAME_NAMES,
|
|
GAME_ALIASES
|
|
};
|