Files
broccolini-bot/zammad-discord.js
root 519788c633 Initial commit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 08:22:19 -06:00

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