Sync broccolini-bot: rename from zammad, docs in docs/, security gitignore, remove zammad deps

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
samkintop
2026-02-12 02:56:00 -06:00
parent 08a16b4a75
commit 29a13768f7
37 changed files with 1093 additions and 3229 deletions

179
broccolini-discord.js Normal file
View File

@@ -0,0 +1,179 @@
/**
* 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 { setClient: setDebugClient } = require('./services/debugLog');
// 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 => {
if (interaction.isButton() && interaction.customId.startsWith(BUTTON_PREFIX)) {
const handled = await handleSendAccountInfoToChannel(interaction);
if (handled) return;
}
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;
}
}
if (interaction.isButton()) {
return handleButton(interaction);
}
if (interaction.isModalSubmit() && interaction.customId.startsWith(SETUP_MODAL_PREFIX)) {
const handled = await handleSetupModal(interaction);
if (handled) return;
}
if (interaction.isModalSubmit() && ['ticket_modal', 'ticket_modal_thread', 'ticket_modal_channel'].includes(interaction.customId)) {
return handleTicketModal(interaction);
}
if (interaction.customId?.startsWith(SETUP_SELECT_PREFIX) && (interaction.isRoleSelectMenu() || interaction.isChannelSelectMenu())) {
const handled = await handleSetupSelect(interaction);
if (handled) return;
}
if (interaction.isChatInputCommand()) {
return handleCommand(interaction);
}
if (interaction.isMessageContextMenuCommand() || interaction.isUserContextMenuCommand()) {
return handleContextMenu(interaction);
}
if (interaction.isAutocomplete()) {
return handleAutocomplete(interaction);
}
});
client.on('messageCreate', handleDiscordReply);
client.once('ready', async () => {
if (!process.env.MONGODB_URI) {
console.error('MONGODB_URI is not set in .env. Broccolini Bot requires MongoDB.');
process.exit(1);
}
await connectMongoDB(process.env.MONGODB_URI);
setDebugClient(client);
console.log(`Broccolini Bot 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);
setInterval(() => poll(client), 30000);
poll(client);
if (CONFIG.AUTO_CLOSE_ENABLED) {
setInterval(() => checkAutoClose(client, sendTicketClosedEmail), 60 * 60 * 1000);
checkAutoClose(client, sendTicketClosedEmail);
console.log('✓ Auto-close enabled: checking every hour');
}
if (CONFIG.REMINDER_ENABLED) {
setInterval(() => checkReminders(client), 30 * 60 * 1000);
checkReminders(client);
console.log('✓ Reminders enabled: checking every 30 minutes');
}
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);
const app = express();
app.get('/', (req, res) => res.send('Active'));
const healthcheckHost = CONFIG.HEALTHCHECK_HOST || undefined;
app.listen(CONFIG.PORT, healthcheckHost, () => {
console.log(`Healthcheck server listening on ${healthcheckHost || '*'}:${CONFIG.PORT}`);
});
module.exports = {
client,
sendGmailReply,
sendTicketClosedEmail,
getNextTicketNumber,
getCleanBody,
detectGame,
stripEmailQuotes,
stripMobileFooter,
htmlToTextWithBlocks
};