203
zammad-discord.js
Normal file
203
zammad-discord.js
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
Reference in New Issue
Block a user