const { Client, GatewayIntentBits, REST, Routes, Collection, EmbedBuilder, ChannelType, PermissionFlagsBits, AuditLogEvent, } = require("discord.js"); const { createClient } = require("@supabase/supabase-js"); const http = require("http"); const fs = require("fs"); const path = require("path"); require("dotenv").config(); const token = process.env.DISCORD_BOT_TOKEN || process.env.DISCORD_TOKEN; const clientId = process.env.DISCORD_CLIENT_ID; if (!token) { console.error("Missing DISCORD_BOT_TOKEN or DISCORD_TOKEN"); process.exit(1); } const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildModeration, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.DirectMessages, ], }); let supabase = null; if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) { supabase = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE ); console.log("Supabase connected"); } client.commands = new Collection(); const commandsPath = path.join(__dirname, "commands"); if (fs.existsSync(commandsPath)) { const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js")); for (const file of commandFiles) { const filePath = path.join(commandsPath, file); const command = require(filePath); if ("data" in command && "execute" in command) { client.commands.set(command.data.name, command); console.log(`Loaded command: ${command.data.name}`); } } } const eventsPath = path.join(__dirname, "events"); if (fs.existsSync(eventsPath)) { const eventFiles = fs.readdirSync(eventsPath).filter((file) => file.endsWith(".js")); for (const file of eventFiles) { const filePath = path.join(eventsPath, file); const event = require(filePath); if ("name" in event && "execute" in event) { client.on(event.name, (...args) => event.execute(...args, client, supabase)); console.log(`Loaded event: ${event.name}`); } } } const sentinelPath = path.join(__dirname, "listeners", "sentinel"); if (fs.existsSync(sentinelPath)) { const sentinelFiles = fs.readdirSync(sentinelPath).filter((file) => file.endsWith(".js")); for (const file of sentinelFiles) { const filePath = path.join(sentinelPath, file); const listener = require(filePath); if ("name" in listener && "execute" in listener) { client.on(listener.name, (...args) => listener.execute(...args, client)); console.log(`Loaded sentinel listener: ${listener.name}`); } } } const heatMap = new Map(); const HEAT_THRESHOLD = 3; const HEAT_WINDOW_MS = 10000; const DANGEROUS_ACTIONS = ['CHANNEL_DELETE', 'ROLE_DELETE', 'MEMBER_BAN_ADD', 'MEMBER_KICK']; const whitelistedUsers = (process.env.WHITELISTED_USERS || '').split(',').filter(Boolean); function addHeat(userId, action) { if (whitelistedUsers.includes(userId)) return 0; const now = Date.now(); if (!heatMap.has(userId)) { heatMap.set(userId, []); } const userEvents = heatMap.get(userId); userEvents.push({ action, timestamp: now }); const recentEvents = userEvents.filter(e => now - e.timestamp < HEAT_WINDOW_MS); heatMap.set(userId, recentEvents); return recentEvents.length; } function getHeat(userId) { const now = Date.now(); const userEvents = heatMap.get(userId) || []; return userEvents.filter(e => now - e.timestamp < HEAT_WINDOW_MS).length; } client.heatMap = heatMap; client.addHeat = addHeat; client.getHeat = getHeat; client.HEAT_THRESHOLD = HEAT_THRESHOLD; const federationMappings = new Map(); client.federationMappings = federationMappings; const REALM_GUILDS = { hub: process.env.HUB_GUILD_ID, labs: process.env.LABS_GUILD_ID, gameforge: process.env.GAMEFORGE_GUILD_ID, corp: process.env.CORP_GUILD_ID, foundation: process.env.FOUNDATION_GUILD_ID, }; client.REALM_GUILDS = REALM_GUILDS; const activeTickets = new Map(); client.activeTickets = activeTickets; let alertChannelId = process.env.ALERT_CHANNEL_ID; client.alertChannelId = alertChannelId; async function sendAlert(message, embed = null) { if (!alertChannelId) return; try { const channel = await client.channels.fetch(alertChannelId); if (channel) { if (embed) { await channel.send({ content: message, embeds: [embed] }); } else { await channel.send(message); } } } catch (err) { console.error("Failed to send alert:", err.message); } } client.sendAlert = sendAlert; client.once("ready", () => { console.log(`Bot logged in as ${client.user.tag}`); console.log(`Watching ${client.guilds.cache.size} server(s)`); client.user.setActivity("Protecting the Federation", { type: 3 }); sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`); }); client.on("interactionCreate", async (interaction) => { if (interaction.isChatInputCommand()) { const command = client.commands.get(interaction.commandName); if (!command) return; try { await command.execute(interaction, supabase, client); } catch (error) { console.error(`Error executing ${interaction.commandName}:`, error); const errorEmbed = new EmbedBuilder() .setColor(0xff0000) .setTitle("Command Error") .setDescription("There was an error while executing this command."); if (interaction.replied || interaction.deferred) { await interaction.followUp({ embeds: [errorEmbed], ephemeral: true }); } else { await interaction.reply({ embeds: [errorEmbed], ephemeral: true }); } } } if (interaction.isButton()) { const [action, ...params] = interaction.customId.split('_'); if (action === 'ticket') { const ticketAction = params[0]; if (ticketAction === 'close') { const ticketChannelId = params[1]; try { const channel = interaction.channel; if (channel && channel.type === ChannelType.GuildText) { await interaction.reply({ content: 'Closing ticket...', ephemeral: true }); setTimeout(() => channel.delete().catch(console.error), 3000); } } catch (err) { console.error('Ticket close error:', err); } } } } }); const healthPort = process.env.HEALTH_PORT || 8080; http.createServer((req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Content-Type", "application/json"); if (req.url === "/health") { res.writeHead(200); res.end(JSON.stringify({ status: "online", guilds: client.guilds.cache.size, commands: client.commands.size, uptime: Math.floor(process.uptime()), heatMapSize: heatMap.size, timestamp: new Date().toISOString(), })); return; } if (req.url === "/stats") { const guildStats = client.guilds.cache.map(g => ({ id: g.id, name: g.name, memberCount: g.memberCount, })); res.writeHead(200); res.end(JSON.stringify({ guilds: guildStats, totalMembers: guildStats.reduce((sum, g) => sum + g.memberCount, 0), uptime: Math.floor(process.uptime()), activeTickets: activeTickets.size, heatEvents: heatMap.size, })); return; } res.writeHead(404); res.end(JSON.stringify({ error: "Not found" })); }).listen(healthPort, () => { console.log(`Health server running on port ${healthPort}`); }); client.login(token).catch((error) => { console.error("Failed to login:", error.message); process.exit(1); }); process.on("unhandledRejection", (error) => { console.error("Unhandled Promise Rejection:", error); }); process.on("uncaughtException", (error) => { console.error("Uncaught Exception:", error); process.exit(1); }); module.exports = client;