204 lines
7.0 KiB
JavaScript
204 lines
7.0 KiB
JavaScript
/**
|
||
* 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
|
||
};
|