// bot.js - Discord bot for moderation and utilities // Copyright (C) 2025 Luis Bauza // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. import 'dotenv/config'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import fs from 'node:fs'; import path from 'node:path'; import { Client, Collection, Events, GatewayIntentBits } from 'discord.js'; import Logger from './logger.js'; import { loadCommands } from './utils/commandLoader.js'; import { getAIResponse, isAIEnabled, clearHistory } from './utils/ai.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const logger = new Logger('bot'); const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, ], }); client.commands = new Collection(); const commandsPath = path.join(__dirname, 'commands'); const commands = await loadCommands(commandsPath, logger); commands.forEach(command => { client.commands.set(command.data.name, command); }); client.once(Events.ClientReady, () => { logger.log(`Ready! Logged in as ${client.user.tag}`); }); // Handle messages where the bot is mentioned client.on(Events.MessageCreate, async message => { // Ignore bot messages if (message.author.bot) return; // Check if bot is mentioned const botId = client.user.id; const mentioned = message.mentions.has(botId); if (!mentioned) return; // Don't respond to commands (they start with /) if (message.content.trim().startsWith('/')) return; if (!isAIEnabled()) { logger.warn('AI response requested but NANOGPT_API_KEY not set'); return; } try { // Get the clean prompt (remove the bot mention) const cleanPrompt = message.content .replace(new RegExp(`<@!?${botId}>`, 'g'), '') .trim(); if (!cleanPrompt) return; // Get username of who mentioned us const mentionedUsername = message.author.username; logger.log(`Mentioned by ${mentionedUsername} in #${message.channel.name}: ${cleanPrompt.substring(0, 50)}...`); // Typing indicator await message.channel.sendTyping(); const response = await getAIResponse(cleanPrompt, message.channel.id, mentionedUsername); // Send response (avoiding mention loop) await message.reply({ content: response, allowedMentions: { users: [] }, // Prevent mentioning the user back }); } catch (error) { logger.error('AI response failed:', error.message); await message.reply({ content: 'lol damn, something broke. try again in a sec', allowedMentions: { users: [] }, }); } }); client.on(Events.InteractionCreate, async interaction => { if (!interaction.isChatInputCommand()) return; const command = client.commands.get(interaction.commandName); if (!command) { logger.error(`No command matching ${interaction.commandName} was found.`); return; } try { await command.execute(interaction); } catch (error) { // Log detailed error context logger.error( `Command "${interaction.commandName}" failed for user ${interaction.user.tag}:`, error, ); // Prepare error response const isProduction = process.env.NODE_ENV === 'production'; const errorMessage = isProduction ? `❌ Command failed. Please try again later.` : `❌ Command failed: ${error.message}\n\n${error.stack}`; const responseMethod = interaction.replied || interaction.deferred ? 'followUp' : 'reply'; await interaction[responseMethod]({ content: errorMessage, ephemeral: true, }); } }); client.login(process.env.DISCORD_TOKEN);