Files
broccolini-bot/config.js
indifferentketchup c5d7539677 staff notifications
2026-04-06 23:53:32 -05:00

217 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Broccolini Bot configuration and game lists.
* Load dotenv so env is available when this module is required first.
* dotenv-expand resolves ${NGROK_URL} etc. in .env.
*
* Test env: set ENV_FILE=.env.test to load .env.test instead of .env (see ENV_AND_SECURITY.md).
* Never commit .env or .env.test; agents must not modify .env without explicit user confirmation.
*/
const path = require('path');
const dotenv = require('dotenv');
const dotenvExpand = require('dotenv-expand');
const envPath = process.env.ENV_FILE
? path.resolve(process.cwd(), process.env.ENV_FILE)
: undefined;
let parsed = dotenv.config({ path: envPath, debug: process.env.NODE_ENV === 'development' });
if (envPath && parsed.error) {
console.warn(`[config] ENV_FILE=${process.env.ENV_FILE} not found or unreadable:`, parsed.error.message);
}
dotenvExpand.expand(parsed);
// If no ENV_FILE, also load repo root .env; only non-empty values override (so empty DISCORD_BOT_TOKEN= in root does not wipe app .env)
if (!envPath) {
const rootEnv = path.resolve(process.cwd(), '..', '.env');
const rootParsed = dotenv.config({ path: rootEnv });
if (!rootParsed.error && rootParsed.parsed) {
for (const [k, v] of Object.entries(rootParsed.parsed)) {
if (v != null && String(v).trim() !== '') process.env[k] = v;
}
dotenvExpand.expand(rootParsed);
}
}
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',
TICKET_T2_CATEGORY_NAME: process.env.TICKET_T2_CATEGORY_NAME || 'Tier 2 Escalated Tickets',
TICKET_T3_CATEGORY_NAME: process.env.TICKET_T3_CATEGORY_NAME || 'Tier 3 Escalated Tickets',
EMAIL_TICKET_OVERFLOW_CATEGORY_IDS: (process.env.EMAIL_TICKET_OVERFLOW_CATEGORY_IDS || '')
.split(',')
.map(s => s.trim())
.filter(Boolean),
DISCORD_TICKET_CATEGORY_ID: process.env.DISCORD_TICKET_CATEGORY_ID || process.env.TICKET_CATEGORY_ID,
DISCORD_TICKET_OVERFLOW_CATEGORY_IDS: (process.env.DISCORD_TICKET_OVERFLOW_CATEGORY_IDS || '')
.split(',')
.map(s => s.trim())
.filter(Boolean),
ROLE_ID_TO_PING: process.env.ROLE_ID_TO_PING,
ROLE_TO_PING_ID: process.env.ROLE_ID_TO_PING || process.env.ROLE_TO_PING_ID,
TRANSCRIPT_CHAN: process.env.TRANSCRIPT_CHANNEL_ID,
LOG_CHAN: process.env.LOGGING_CHANNEL_ID,
DEBUGGING_CHANNEL_ID: process.env.DEBUGGING_CHANNEL_ID || null,
BACKUP_EXPORT_CHANNEL_ID: process.env.BACKUP_EXPORT_CHANNEL_ID || null,
ACCOUNT_INFO_CHANNEL_ID: process.env.ACCOUNT_INFO_CHANNEL_ID || null,
DISCORD_CHANNEL_ID: process.env.DISCORD_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',
PORT: 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
SIGNATURE: (process.env.EMAIL_SIGNATURE || '').trim().replace(/\\n/g, '<br>'),
GAME_LIST: process.env.GAME_LIST || '',
DISCORD_THREAD_CHANNEL_ID: process.env.DISCORD_THREAD_CHANNEL_ID || null,
EMAIL_THREAD_CHANNEL_ID: process.env.EMAIL_THREAD_CHANNEL_ID || null,
EMAIL_ESCALATED_CATEGORY_ID: process.env.EMAIL_ESCALATED_CATEGORY_ID || process.env.ESCALATED_CATEGORY_ID,
DISCORD_ESCALATED_CATEGORY_ID: process.env.DISCORD_ESCALATED_CATEGORY_ID,
// Tier 2/3 email 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.',
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 || 'This ticket has been marked 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: parseInt(process.env.AUTO_CLOSE_AFTER_HOURS) || 72,
AUTO_CLOSE_MESSAGE: process.env.AUTO_CLOSE_MESSAGE || 'This ticket has been automatically closed due to inactivity.',
GLOBAL_TICKET_LIMIT: parseInt(process.env.GLOBAL_TICKET_LIMIT) || 5,
TICKET_LIMIT_PER_CATEGORY: parseInt(process.env.TICKET_LIMIT_PER_CATEGORY) || 3,
RATE_LIMIT_TICKETS_PER_USER: parseInt(process.env.RATE_LIMIT_TICKETS_PER_USER) || 0,
RATE_LIMIT_WINDOW_MINUTES: parseInt(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),
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} ☀️',
REMINDER_ENABLED: process.env.REMINDER_ENABLED === 'true',
REMINDER_AFTER_HOURS: parseInt(process.env.REMINDER_AFTER_HOURS) || 24,
REMINDER_MESSAGE: process.env.REMINDER_MESSAGE || 'Hey {ping}! This ticket has been inactive for {hours} hours. Please provide an update or close the ticket.',
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 || '🟢',
CLAIM_TIMEOUT_ENABLED: process.env.CLAIM_TIMEOUT_ENABLED === 'true',
CLAIM_TIMEOUT_HOURS: parseInt(process.env.CLAIM_TIMEOUT_HOURS) || 48,
AUTO_UNCLAIM_ENABLED: process.env.AUTO_UNCLAIM_ENABLED === 'true',
AUTO_UNCLAIM_AFTER_HOURS: parseInt(process.env.AUTO_UNCLAIM_AFTER_HOURS) || 24,
ALLOW_CLAIM_OVERWRITE: process.env.ALLOW_CLAIM_OVERWRITE === 'true',
USE_THREADS: process.env.USE_THREADS === 'true',
THREAD_PARENT_CHANNEL: process.env.THREAD_PARENT_CHANNEL || process.env.EMAIL_THREAD_CHANNEL_ID || null,
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: parseInt(process.env.EMBED_COLOR_OPEN) || 0x00FF00,
EMBED_COLOR_CLOSED: parseInt(process.env.EMBED_COLOR_CLOSED) || 0xFF0000,
EMBED_COLOR_CLAIMED: parseInt(process.env.EMBED_COLOR_CLAIMED) || 0xFFFF00,
EMBED_COLOR_ESCALATED: parseInt(process.env.EMBED_COLOR_ESCALATED) || 0xFF6600,
EMBED_COLOR_INFO: parseInt(process.env.EMBED_COLOR_INFO) || 0x1e2124,
STAFF_CATEGORIES: new Map(), // deprecated kept for staffChannel.js compat
STAFF_EMOJIS: (() => {
const raw = process.env.STAFF_EMOJIS;
const map = new Map();
if (!raw || !String(raw).trim()) return map;
for (const part of String(raw).split(',')) {
const seg = part.trim();
if (!seg) continue;
const idx = seg.indexOf(':');
if (idx === -1) continue;
const userId = seg.slice(0, idx).trim();
const emoji = seg.slice(idx + 1).trim();
if (userId && emoji) map.set(userId, emoji);
}
return map;
})(),
CLAIMER_EMOJI_FALLBACK: process.env.CLAIMER_EMOJI_FALLBACK || '🎫',
ADMIN_ID: process.env.ADMIN_ID || null,
STAFF_NOTIFICATION_CATEGORY_ID: process.env.STAFF_NOTIFICATION_CATEGORY_ID || null,
UNCLAIMED_REMINDER_THRESHOLDS: (process.env.UNCLAIMED_REMINDER_THRESHOLDS || '1,2,4')
.split(',')
.map(s => parseInt(s.trim(), 10))
.filter(n => !isNaN(n) && n > 0)
};
/** Ticket category tags for /tag set [emoji] [label] in dropdown; priority emoji always first in channel name, then tag emoji. */
const TICKET_TAGS = [
{ value: 'server-down', emoji: '⬇️', name: 'Server Down' },
{ value: 'stuck-restarting', emoji: '⏳', name: 'Stuck Restarting' },
{ value: 'cant-connect', emoji: '📵', name: "Can't Connect" },
{ value: 'server-lag', emoji: '🐌', name: 'Server Lag' },
{ value: 'billing', emoji: '💳', name: 'Billing' },
{ value: 'refund-request', emoji: '💸', name: 'Refund Request' },
{ value: 'mod-help', emoji: '🔧', name: 'Mod Help' },
{ value: 'backup-restore', emoji: '💾', name: 'Backup Restore' },
{ value: 'world-save', emoji: '🌍', name: 'World / Save' },
{ value: 'server-config', emoji: '⚙️', name: 'Server Config' }
];
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'
};
const GAME_NAME_TO_KEY = {
'Project Zomboid': 'project_zomboid',
'Satisfactory': 'satisfactory',
'Palworld': 'palworld',
'Minecraft': 'minecraft',
'Valheim': 'valheim',
'Enshrouded': 'enshrouded',
'7 Days to Die': '7_days_to_die',
'Hytale': 'hytale',
'ICARUS': 'icarus',
'Abiotic Factor': 'abiotic_factor',
'ARK: Survival Evolved': 'ark_survival_evolved',
'Conan Exiles': 'conan_exiles',
'Core Keeper': 'core_keeper',
'Counter-Strike 2': 'counter_strike_2',
'DayZ': 'dayz',
'ECO': 'eco',
'Factorio': 'factorio',
'FiveM': 'fivem',
'The Front': 'the_front',
"Garry's Mod": 'garrys_mod',
'Necesse': 'necesse',
'Rust': 'rust',
'Sons of the Forest': 'sons_of_the_forest',
'Soulmask': 'soulmask',
'Star Rupture': 'star_rupture',
'Terraria': 'terraria',
'VEIN': 'vein',
'Vintage Story': 'vintage_story',
'Voyagers of Nera': 'voyagers_of_nera',
'V Rising': 'v_rising'
};
module.exports = {
CONFIG,
TICKET_TAGS,
GAME_NAMES,
GAME_ALIASES,
GAME_NAME_TO_KEY
};