Initial commit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
root
2026-02-10 08:22:19 -06:00
commit 519788c633
39 changed files with 17121 additions and 0 deletions

203
zammad-discord.js Normal file
View 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
};