355 lines
18 KiB
JavaScript
355 lines
18 KiB
JavaScript
/**
|
||
* 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 DEFAULT_NOTIFICATION_THRESHOLDS = {
|
||
// patternChecker - age-based (time since condition first became true)
|
||
user_tickets: ['15m', '30m', '1h', '3h'],
|
||
user_reopen: ['1h', '4h', '1d'],
|
||
user_crossgame: ['1h', '1d'],
|
||
game_surge: ['15m', '30m', '1h'],
|
||
game_backlog: ['30m', '1h', '3h', '6h'],
|
||
game_resolution: ['1d'],
|
||
game_spike: ['15m', '30m'],
|
||
tag_top: ['1h', '6h', '1d'],
|
||
tag_escalation: ['1h', '6h', '1d'],
|
||
untagged_closes: ['1h', '1d'],
|
||
tag_game_corr: ['1d'],
|
||
user_esc: ['1h', '6h', '1d'],
|
||
game_esc_rate: ['1d'],
|
||
rapid_t2_t3: ['3', '5', '10', '15', '20', '30', '50'], // count-based milestones, not time
|
||
staff_no_close: ['1h', '3h'],
|
||
staff_overloaded: ['1h', '3h', '6h'],
|
||
staff_stale: ['1h', '3h'],
|
||
staff_transfer_rate: ['1h', '1d'],
|
||
staff_esc: ['1h', '6h', '1d'],
|
||
staff_game_esc: ['1d'],
|
||
game_tag_spike: ['1h', '6h'],
|
||
overnight_gap: ['1d'],
|
||
staff_always_esc: ['1d'],
|
||
// surgeChecker - cooldown-escalating (repeat alerts spaced further apart)
|
||
surge_tickets: ['10m', '30m', '1h', '2h', '3h'],
|
||
surge_game: ['10m', '30m', '1h', '2h'],
|
||
surge_stale: ['30m', '1h', '2h', '4h'],
|
||
surge_needs_response: ['15m', '30m', '1h', '3h'],
|
||
surge_unclaimed: ['15m', '30m', '1h', '2h', '4h'],
|
||
surge_tier3_unclaimed: ['10m', '15m', '30m', '1h', '2h'],
|
||
surge_no_staff: ['10m', '20m', '30m', '1h'],
|
||
// staffNotifications - age-based per ticket (hours)
|
||
unclaimed_reminder: ['1h', '2h', '4h', '8h', '1d'],
|
||
// chatAlertChecker - cooldown-escalating
|
||
chat_messages: ['15m', '30m', '1h', '3h'],
|
||
chat_time: ['30m', '1h', '2h', '4h']
|
||
};
|
||
|
||
function toInt(v, fallback) {
|
||
const n = parseInt(v, 10);
|
||
return Number.isFinite(n) ? n : fallback;
|
||
}
|
||
|
||
function parseThresholdString(str) {
|
||
const value = String(str || '').trim();
|
||
if (!value) return NaN;
|
||
|
||
// Integers without a unit are raw count milestones.
|
||
if (/^\d+$/.test(value)) return parseInt(value, 10);
|
||
|
||
let totalMs = 0;
|
||
const re = /(\d+)([mhd])/g;
|
||
let match;
|
||
let consumed = '';
|
||
while ((match = re.exec(value)) !== null) {
|
||
const amount = parseInt(match[1], 10);
|
||
const unit = match[2];
|
||
consumed += match[0];
|
||
if (unit === 'm') totalMs += amount * 60 * 1000;
|
||
else if (unit === 'h') totalMs += amount * 60 * 60 * 1000;
|
||
else if (unit === 'd') totalMs += amount * 24 * 60 * 60 * 1000;
|
||
}
|
||
|
||
if (!consumed || consumed !== value) return NaN;
|
||
return totalMs;
|
||
}
|
||
|
||
function parseNotificationThresholdsJson(raw) {
|
||
if (!raw || !String(raw).trim()) return DEFAULT_NOTIFICATION_THRESHOLDS;
|
||
try {
|
||
const parsedJson = JSON.parse(raw);
|
||
if (parsedJson && typeof parsedJson === 'object' && !Array.isArray(parsedJson)) {
|
||
return parsedJson;
|
||
}
|
||
} catch (err) {
|
||
console.warn('[config] Failed to parse NOTIFICATION_THRESHOLDS_JSON, using default:', err.message);
|
||
}
|
||
return DEFAULT_NOTIFICATION_THRESHOLDS;
|
||
}
|
||
|
||
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: 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
|
||
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,
|
||
// 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.',
|
||
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: toInt(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: toInt(process.env.GLOBAL_TICKET_LIMIT, 5),
|
||
TICKET_LIMIT_PER_CATEGORY: toInt(process.env.TICKET_LIMIT_PER_CATEGORY, 3),
|
||
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),
|
||
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: toInt(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: toInt(process.env.CLAIM_TIMEOUT_HOURS, 48),
|
||
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',
|
||
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: toInt(process.env.EMBED_COLOR_OPEN, 0x00FF00),
|
||
EMBED_COLOR_CLOSED: toInt(process.env.EMBED_COLOR_CLOSED, 0xFF0000),
|
||
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),
|
||
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,
|
||
FORCE_CLOSE_TIMER: toInt(process.env.FORCE_CLOSE_TIMER_SECONDS, 60),
|
||
GMAIL_POLL_INTERVAL_MS: toInt(process.env.GMAIL_POLL_INTERVAL_SECONDS, 30) * 1000,
|
||
GMAIL_LOG_CHANNEL_ID: process.env.GMAIL_LOG_CHANNEL_ID || null,
|
||
AUTOMATION_LOG_CHANNEL_ID: process.env.AUTOMATION_LOG_CHANNEL_ID || null,
|
||
RENAME_LOG_CHANNEL_ID: process.env.RENAME_LOG_CHANNEL_ID || null,
|
||
SECURITY_LOG_CHANNEL_ID: process.env.SECURITY_LOG_CHANNEL_ID || null,
|
||
SYSTEM_LOG_CHANNEL_ID: process.env.SYSTEM_LOG_CHANNEL_ID || null,
|
||
USER_PATTERNS_CHANNEL_ID: process.env.USER_PATTERNS_CHANNEL_ID || null,
|
||
GAME_PATTERNS_CHANNEL_ID: process.env.GAME_PATTERNS_CHANNEL_ID || null,
|
||
TAG_PATTERNS_CHANNEL_ID: process.env.TAG_PATTERNS_CHANNEL_ID || null,
|
||
ESCALATION_PATTERNS_CHANNEL_ID: process.env.ESCALATION_PATTERNS_CHANNEL_ID || null,
|
||
STAFF_PATTERNS_CHANNEL_ID: process.env.STAFF_PATTERNS_CHANNEL_ID || null,
|
||
COMBINED_PATTERNS_CHANNEL_ID: process.env.COMBINED_PATTERNS_CHANNEL_ID || null,
|
||
PATTERN_USER_TICKET_THRESHOLD: toInt(process.env.PATTERN_USER_TICKET_THRESHOLD, 3),
|
||
PATTERN_GAME_TICKET_THRESHOLD: toInt(process.env.PATTERN_GAME_TICKET_THRESHOLD, 10),
|
||
PATTERN_STAFF_STALE_PING_THRESHOLD: toInt(process.env.PATTERN_STAFF_STALE_PING_THRESHOLD, 5),
|
||
PATTERN_ESCALATION_THRESHOLD: toInt(process.env.PATTERN_ESCALATION_THRESHOLD, 3),
|
||
PATTERN_RAPID_CLOSE_SECONDS: toInt(process.env.PATTERN_RAPID_CLOSE_SECONDS, 120),
|
||
PATTERN_UNCLAIMED_HOURS: toInt(process.env.PATTERN_UNCLAIMED_HOURS, 4),
|
||
PATTERN_CHECK_INTERVAL_MINUTES: toInt(process.env.PATTERN_CHECK_INTERVAL_MINUTES, 30),
|
||
ALL_STAFF_CHANNEL_ID: process.env.ALL_STAFF_CHANNEL_ID || null,
|
||
ALL_STAFF_CHAT_ALERT_CHANNEL_ID: process.env.ALL_STAFF_CHAT_ALERT_CHANNEL_ID || null,
|
||
SURGE_ROLE_ID: process.env.SURGE_ROLE_ID || null,
|
||
SURGE_TICKET_COUNT: toInt(process.env.SURGE_TICKET_COUNT, 10),
|
||
SURGE_TICKET_WINDOW_MINUTES: toInt(process.env.SURGE_TICKET_WINDOW_MINUTES, 30),
|
||
SURGE_GAME_TICKET_COUNT: toInt(process.env.SURGE_GAME_TICKET_COUNT, 5),
|
||
SURGE_GAME_TICKET_WINDOW_MINUTES: toInt(process.env.SURGE_GAME_TICKET_WINDOW_MINUTES, 30),
|
||
SURGE_STALE_COUNT: toInt(process.env.SURGE_STALE_COUNT, 8),
|
||
SURGE_STALE_HOURS: toInt(process.env.SURGE_STALE_HOURS, 2),
|
||
SURGE_NEEDS_RESPONSE_COUNT: toInt(process.env.SURGE_NEEDS_RESPONSE_COUNT, 5),
|
||
SURGE_NEEDS_RESPONSE_HOURS: toInt(process.env.SURGE_NEEDS_RESPONSE_HOURS, 1),
|
||
SURGE_UNCLAIMED_COUNT: toInt(process.env.SURGE_UNCLAIMED_COUNT, 5),
|
||
SURGE_UNCLAIMED_MINUTES: toInt(process.env.SURGE_UNCLAIMED_MINUTES, 30),
|
||
SURGE_TIER3_UNCLAIMED_MINUTES: toInt(process.env.SURGE_TIER3_UNCLAIMED_MINUTES, 15),
|
||
SURGE_COOLDOWN_MINUTES: toInt(process.env.SURGE_COOLDOWN_MINUTES, 60),
|
||
CHAT_ALERT_CHANNEL_IDS: (process.env.CHAT_ALERT_CHANNEL_IDS || '').split(',').filter(Boolean),
|
||
CHAT_ALERT_MESSAGE_COUNT: toInt(process.env.CHAT_ALERT_MESSAGE_COUNT, 5),
|
||
CHAT_ALERT_HOURS_WITHOUT_RESPONSE: toInt(process.env.CHAT_ALERT_HOURS_WITHOUT_RESPONSE, 2),
|
||
CHAT_ALERT_COOLDOWN_MINUTES: toInt(process.env.CHAT_ALERT_COOLDOWN_MINUTES, 60),
|
||
STAFF_IDS: (process.env.STAFF_IDS || '').split(',').map(s => s.trim()).filter(Boolean),
|
||
SURGE_NO_STAFF_COOLDOWN_MINUTES: toInt(process.env.SURGE_NO_STAFF_COOLDOWN_MINUTES, 30),
|
||
SURGE_NO_STAFF_OPEN_TICKET_THRESHOLD: toInt(process.env.SURGE_NO_STAFF_OPEN_TICKET_THRESHOLD, 3),
|
||
STAFF_DND_COUNTS_AS_AVAILABLE: process.env.STAFF_DND_COUNTS_AS_AVAILABLE === 'true',
|
||
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',
|
||
SETTINGS_PORT: toInt(process.env.SETTINGS_PORT, 12752),
|
||
SETTINGS_ADMIN_PASSWORD: process.env.SETTINGS_ADMIN_PASSWORD || null,
|
||
SETTINGS_DOMAIN: process.env.SETTINGS_DOMAIN || 'tickets.indifferentketchup.com',
|
||
INTERNAL_API_PORT: toInt(process.env.INTERNAL_API_PORT, 12753),
|
||
INTERNAL_API_SECRET: process.env.INTERNAL_API_SECRET || null,
|
||
NOTIFICATION_THRESHOLDS: parseNotificationThresholdsJson(process.env.NOTIFICATION_THRESHOLDS_JSON),
|
||
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,
|
||
parseThresholdString,
|
||
TICKET_TAGS,
|
||
GAME_NAMES,
|
||
GAME_ALIASES,
|
||
GAME_NAME_TO_KEY
|
||
};
|