/** * Entry point – initializes the Discord bot, wires event handlers, * connects to MongoDB, starts Gmail polling, and runs the Express healthcheck. */ const { Client, GatewayIntentBits, Partials } = require('discord.js'); const express = require('express'); const { connectMongoDB } = require('./db-connection'); const { CONFIG } = require('./config'); // Handlers const { handleButton, handleTicketModal } = require('./handlers/buttons'); const { handleCommand, handleContextMenu, handleAutocomplete } = require('./handlers/commands'); const { handleSendAccountInfoToChannel, BUTTON_PREFIX } = require('./handlers/accountinfo'); const { handleSetupButton, handleSetupModal, handleSetupSelect, PREFIX_BUTTON: SETUP_BUTTON_PREFIX, PREFIX_MODAL: SETUP_MODAL_PREFIX, PREFIX_SELECT: SETUP_SELECT_PREFIX } = require('./handlers/setup'); const { handleDiscordReply } = require('./handlers/messages'); // Services & jobs const { sendTicketClosedEmail } = require('./services/gmail'); const { checkAutoClose, checkReminders, checkAutoUnclaim } = require('./services/tickets'); const { registerCommands } = require('./commands/register'); const { poll } = require('./gmail-poll'); const { syncZammadReplies } = require('./services/zammad-sync'); const { setClient: setDebugClient } = require('./services/debugLog'); const { ZAMMAD } = require('./config'); // Re-export utilities for any external consumers const { sendGmailReply } = require('./services/gmail'); const { getNextTicketNumber } = require('./services/tickets'); const { getCleanBody, detectGame, stripEmailQuotes, stripMobileFooter, htmlToTextWithBlocks } = require('./utils'); // --- VALIDATE CONFIG --- if (!CONFIG.DISCORD_TOKEN) { console.error('DISCORD_TOKEN is not set in .env'); process.exit(1); } if (!CONFIG.TICKET_CATEGORY_ID) { console.error('TICKET_CATEGORY_ID is not set in .env – cannot create ticket channels.'); process.exit(1); } if (!CONFIG.CLIENT_ID) { console.error('DISCORD_APPLICATION_ID is not set in .env – cannot register slash commands.'); } if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) { console.error('GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET is not set in .env – Gmail OAuth may fail.'); } // --- DISCORD CLIENT --- const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers ], partials: [Partials.Channel] }); // --- EVENT: interactionCreate --- client.on('interactionCreate', async interaction => { // Account-info "send to channel" button if (interaction.isButton() && interaction.customId.startsWith(BUTTON_PREFIX)) { const handled = await handleSendAccountInfoToChannel(interaction); if (handled) return; } // Setup wizard buttons (must run before generic button handler so we don't hit "Data missing.") if (interaction.isButton() && interaction.customId.startsWith(SETUP_BUTTON_PREFIX)) { try { const handled = await handleSetupButton(interaction); if (handled) return; } catch (err) { console.error('Setup button error:', err); await interaction.reply({ content: `Setup error: ${err.message}. Try \`/setup\` again.`, ephemeral: true }).catch(() => {}); return; } } // Buttons (includes open_ticket, claim, close, priority, tag-delete, etc.) if (interaction.isButton()) { return handleButton(interaction); } // Setup wizard modal (panel name) if (interaction.isModalSubmit() && interaction.customId.startsWith(SETUP_MODAL_PREFIX)) { const handled = await handleSetupModal(interaction); if (handled) return; } // Modal submissions (ticket_modal from the panel button) if (interaction.isModalSubmit() && ['ticket_modal', 'ticket_modal_thread', 'ticket_modal_channel'].includes(interaction.customId)) { return handleTicketModal(interaction); } // Setup wizard select menus (roles, category, transcript channel, panel channel) if (interaction.customId?.startsWith(SETUP_SELECT_PREFIX) && (interaction.isRoleSelectMenu() || interaction.isChannelSelectMenu())) { const handled = await handleSetupSelect(interaction); if (handled) return; } // Slash commands if (interaction.isChatInputCommand()) { return handleCommand(interaction); } // Context menu commands if (interaction.isMessageContextMenuCommand() || interaction.isUserContextMenuCommand()) { return handleContextMenu(interaction); } // Autocomplete if (interaction.isAutocomplete()) { return handleAutocomplete(interaction); } }); // --- EVENT: messageCreate (Discord → Gmail reply) --- client.on('messageCreate', handleDiscordReply); // --- EVENT: ready --- client.once('ready', async () => { if (!process.env.MONGODB_URI) { console.error('MONGODB_URI is not set in .env. Bridge requires MongoDB.'); process.exit(1); } await connectMongoDB(process.env.MONGODB_URI); setDebugClient(client); console.log(`gmail-discord instance active on port ${CONFIG.PORT}`); const guild = CONFIG.DISCORD_GUILD_ID ? client.guilds.cache.get(CONFIG.DISCORD_GUILD_ID) : client.guilds.cache.first(); if (!guild) { console.warn('No guild found on ready.'); } else { const parent = guild.channels.cache.get(CONFIG.TICKET_CATEGORY_ID); console.log('Ticket parent lookup:', { id: CONFIG.TICKET_CATEGORY_ID, exists: !!parent, type: parent?.type }); } registerCommands().catch(console.error); // Gmail polling every 30 seconds setInterval(() => poll(client), 30000); poll(client); // Zammad reply sync: push agent replies from Zammad to Discord/Gmail every 30 seconds if (ZAMMAD?.URL && ZAMMAD?.TOKEN) { setInterval(() => syncZammadReplies(client), 30000); syncZammadReplies(client); console.log('✓ Zammad reply sync enabled: every 30 seconds'); } // Auto-close check every hour if (CONFIG.AUTO_CLOSE_ENABLED) { setInterval(() => checkAutoClose(client, sendTicketClosedEmail), 60 * 60 * 1000); checkAutoClose(client, sendTicketClosedEmail); console.log('✓ Auto-close enabled: checking every hour'); } // Reminder check every 30 minutes if (CONFIG.REMINDER_ENABLED) { setInterval(() => checkReminders(client), 30 * 60 * 1000); checkReminders(client); console.log('✓ Reminders enabled: checking every 30 minutes'); } // Auto-unclaim check every hour if (CONFIG.AUTO_UNCLAIM_ENABLED) { setInterval(() => checkAutoUnclaim(client), 60 * 60 * 1000); checkAutoUnclaim(client); console.log('✓ Auto-unclaim enabled: checking every hour'); } console.log('✓ Discord bot ready. Tag:', client.user.tag); }); client.login(CONFIG.DISCORD_TOKEN); // --- HEALTHCHECK --- const app = express(); app.get('/', (req, res) => res.send('Active')); app.listen(CONFIG.PORT, () => { console.log(`Healthcheck server listening on ${CONFIG.PORT}`); }); module.exports = { client, sendGmailReply, sendTicketClosedEmail, getNextTicketNumber, getCleanBody, detectGame, stripEmailQuotes, stripMobileFooter, htmlToTextWithBlocks };