const { Client, GatewayIntentBits, REST, Routes, Collection, EmbedBuilder, ChannelType, PermissionFlagsBits, } = require("discord.js"); const { createClient } = require("@supabase/supabase-js"); const { Kazagumo, Plugins } = require("kazagumo"); const { Connectors } = require("shoukaku"); const http = require("http"); const fs = require("fs"); const path = require("path"); const WebSocket = require("ws"); const { createWebServer } = require("./server/webServer"); require("dotenv").config(); // ============================================================================= // ENVIRONMENT VALIDATION (Modified: Supabase now optional) // ============================================================================= const token = process.env.DISCORD_BOT_TOKEN; const clientId = process.env.DISCORD_CLIENT_ID; if (!token) { console.error("Missing DISCORD_BOT_TOKEN environment variable"); process.exit(1); } if (!clientId) { console.error("Missing DISCORD_CLIENT_ID environment variable"); process.exit(1); } console.log("[Token] Bot token loaded (length: " + token.length + " chars)"); // ============================================================================= // DISCORD CLIENT SETUP (Modified: Added intents for Sentinel) // ============================================================================= const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildModeration, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.DirectMessages, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildVoiceStates, ], }); // ============================================================================= // LAVALINK MUSIC SETUP (Kazagumo + Shoukaku) // ============================================================================= const LavalinkNodes = [ { name: 'lavalink-v4', url: 'lava-v4.ajieblogs.eu.org:443', auth: 'https://dsc.gg/ajidevserver', secure: true }, { name: 'lavalink-serenetia', url: 'lavalinkv4.serenetia.com:443', auth: 'https://dsc.gg/ajidevserver', secure: true } ]; const shoukakuOptions = { moveOnDisconnect: false, resumable: true, resumableTimeout: 60, reconnectTries: 5, restTimeout: 30000, reconnectInterval: 5 }; const kazagumo = new Kazagumo({ defaultSearchEngine: 'youtube', send: (guildId, payload) => { const guild = client.guilds.cache.get(guildId); if (guild) guild.shard.send(payload); } }, new Connectors.DiscordJS(client), LavalinkNodes, shoukakuOptions); kazagumo.shoukaku.on('ready', (name) => { console.log(`[Music] Lavalink node "${name}" connected`); }); kazagumo.shoukaku.on('error', (name, error) => { console.error(`[Music] Lavalink node "${name}" error:`, error.message); }); kazagumo.shoukaku.on('close', (name, code, reason) => { console.warn(`[Music] Lavalink node "${name}" closed: ${code} - ${reason}`); }); kazagumo.shoukaku.on('disconnect', (name, players, moved) => { console.warn(`[Music] Lavalink node "${name}" disconnected`); }); kazagumo.on('playerStart', (player, track) => { const channel = player.textChannel ? client.channels.cache.get(player.textChannel) : null; if (channel) { const duration = track.length ? formatDuration(track.length) : 'Live'; const embed = new EmbedBuilder() .setColor(0x5865f2) .setTitle('Now Playing') .setDescription(`**[${track.title}](${track.uri})**`) .setThumbnail(track.thumbnail || null) .addFields( { name: 'Duration', value: duration, inline: true }, { name: 'Author', value: track.author || 'Unknown', inline: true } ); channel.send({ embeds: [embed] }).catch(() => {}); } }); kazagumo.on('playerEnd', (player) => { if (player.queue.size === 0 && !player.playing) { const channel = player.textChannel ? client.channels.cache.get(player.textChannel) : null; if (channel) { channel.send({ embeds: [new EmbedBuilder() .setColor(0x5865f2) .setDescription('Queue finished. Leaving voice channel...')] }).catch(() => {}); } setTimeout(() => { if (!player.playing && player.queue.size === 0) { player.destroy(); } }, 30000); } }); kazagumo.on('playerEmpty', (player) => { const channel = player.textChannel ? client.channels.cache.get(player.textChannel) : null; if (channel) { channel.send({ embeds: [new EmbedBuilder() .setColor(0x5865f2) .setDescription('Queue finished. Leaving voice channel...')] }).catch(() => {}); } player.destroy(); }); kazagumo.on('playerError', (player, error) => { console.error(`[Music] Player error:`, error); }); function formatDuration(ms) { const seconds = Math.floor((ms / 1000) % 60); const minutes = Math.floor((ms / (1000 * 60)) % 60); const hours = Math.floor(ms / (1000 * 60 * 60)); if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } return `${minutes}:${seconds.toString().padStart(2, '0')}`; } client.kazagumo = kazagumo; console.log('[Music] Kazagumo/Lavalink music system initialized'); // ============================================================================= // SUPABASE SETUP (Modified: Now optional) // ============================================================================= 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"); } else { console.log("Supabase not configured - community features will be limited"); } // Achievement tracking for command usage async function trackCommandForAchievements(discordUserId, guildId, member, supabaseClient, discordClient) { try { const { data: link } = await supabaseClient .from('discord_links') .select('user_id') .eq('discord_id', discordUserId) .maybeSingle(); if (!link) return; const { updateUserStats, getUserStats, calculateLevel, updateQuestProgress } = require('./listeners/xpTracker'); const { checkAchievements } = require('./commands/achievements'); await updateUserStats(supabaseClient, link.user_id, guildId, { commandsUsed: 1 }); // Track quest progress for command usage await updateQuestProgress(supabaseClient, link.user_id, guildId, 'commands', 1); const { data: profile } = await supabaseClient .from('user_profiles') .select('xp, prestige_level, total_xp_earned, daily_streak') .eq('id', link.user_id) .maybeSingle(); if (profile) { const stats = await getUserStats(supabaseClient, link.user_id, guildId); stats.level = calculateLevel(profile.xp || 0, 'normal'); stats.prestige = profile.prestige_level || 0; stats.totalXp = profile.total_xp_earned || profile.xp || 0; stats.dailyStreak = profile.daily_streak || 0; await checkAchievements(link.user_id, member, stats, supabaseClient, guildId, discordClient); } } catch (e) { // Silent fail for achievement tracking } } // ============================================================================= // COMMAND LOGGING SYSTEM (Supabase-based) // ============================================================================= async function logCommand(data) { if (!supabase) return; try { await supabase.from('command_logs').insert({ command_name: data.commandName, user_id: data.userId, user_tag: data.userTag, guild_id: data.guildId, guild_name: data.guildName, channel_id: data.channelId, success: data.success, error_message: data.errorMessage || null, execution_time_ms: data.executionTime || null }); } catch (err) { console.error('Failed to log command:', err.message); } } async function getCommandAnalytics(days = 7) { if (!supabase) return { commands: [], hourly: [], daily: [], topUsers: [] }; try { const cutoffDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); const { data: logs } = await supabase .from('command_logs') .select('*') .gte('created_at', cutoffDate); if (!logs) return { commands: [], hourly: [], daily: [], topUsers: [] }; // Calculate command usage const commandCounts = {}; const userCounts = {}; const hourlyActivity = Array(24).fill(0); const dailyActivity = {}; for (const log of logs) { // Command counts commandCounts[log.command_name] = (commandCounts[log.command_name] || 0) + 1; // User counts const userKey = `${log.user_id}|${log.user_tag}`; userCounts[userKey] = (userCounts[userKey] || 0) + 1; // Hourly const hour = new Date(log.created_at).getHours(); hourlyActivity[hour]++; // Daily const dateKey = new Date(log.created_at).toISOString().split('T')[0]; dailyActivity[dateKey] = (dailyActivity[dateKey] || 0) + 1; } const commands = Object.entries(commandCounts) .map(([name, count]) => ({ command_name: name, count })) .sort((a, b) => b.count - a.count) .slice(0, 20); const topUsers = Object.entries(userCounts) .map(([key, count]) => { const [user_id, user_tag] = key.split('|'); return { user_id, user_tag, command_count: count }; }) .sort((a, b) => b.command_count - a.command_count) .slice(0, 10); const hourly = hourlyActivity.map((count, hour) => ({ hour, count })); const daily = Object.entries(dailyActivity).map(([date, count]) => ({ date, count })); return { commands, hourly, daily, topUsers }; } catch (err) { console.error('Failed to get command analytics:', err.message); return { commands: [], hourly: [], daily: [], topUsers: [] }; } } async function getTotalCommandCount() { if (!supabase) return 0; try { const { count } = await supabase.from('command_logs').select('*', { count: 'exact', head: true }); return count || 0; } catch (err) { return 0; } } // Supabase-based server config functions async function saveServerConfigToDB(guildId, config) { if (!supabase) return false; try { await supabase.from('server_config').upsert({ guild_id: guildId, welcome_channel: config.welcome_channel, goodbye_channel: config.goodbye_channel, modlog_channel: config.modlog_channel, level_up_channel: config.level_up_channel, auto_role: config.auto_role, verified_role: config.verified_role, updated_at: new Date().toISOString() }); return true; } catch (err) { console.error('Failed to save server config:', err.message); return false; } } async function getServerConfigFromDB(guildId) { if (!supabase) return null; try { const { data } = await supabase .from('server_config') .select('*') .eq('guild_id', guildId) .single(); return data || null; } catch (err) { console.error('Failed to get server config:', err.message); return null; } } // Federation mappings with Supabase async function saveFederationMappingToDB(guildId, roleId, roleName) { if (!supabase) return false; try { await supabase.from('federation_mappings').upsert({ guild_id: guildId, role_id: roleId, role_name: roleName, linked_at: new Date().toISOString() }); return true; } catch (err) { console.error('Failed to save federation mapping:', err.message); return false; } } async function getFederationMappingsFromDB() { if (!supabase) return []; try { const { data } = await supabase .from('federation_mappings') .select('*') .order('linked_at', { ascending: false }); return data || []; } catch (err) { console.error('Failed to get federation mappings:', err.message); return []; } } // ============================================================================= // SENTINEL: HEAT TRACKING SYSTEM (New) // ============================================================================= const heatMap = new Map(); const HEAT_THRESHOLD = 3; const HEAT_WINDOW_MS = 10000; 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; // ============================================================================= // SENTINEL: FEDERATION MAPPINGS (New) // ============================================================================= const federationMappings = new Map(); client.federationMappings = federationMappings; async function loadFederationMappings() { if (!supabase) return; try { const { data, error } = await supabase .from('federation_mappings') .select('*'); if (error) throw error; for (const mapping of data || []) { federationMappings.set(mapping.role_id, { name: mapping.role_name, guildId: mapping.guild_id, guildName: mapping.guild_name, linkedAt: new Date(mapping.linked_at).getTime(), }); } console.log(`[Federation] Loaded ${federationMappings.size} mappings from database`); } catch (e) { console.warn('[Federation] Could not load mappings:', e.message); } } async function saveFederationMapping(roleId, data) { if (!supabase) return; try { await supabase.from('federation_mappings').upsert({ role_id: roleId, role_name: data.name, guild_id: data.guildId, guild_name: data.guildName, linked_at: new Date(data.linkedAt).toISOString(), }); } catch (e) { console.warn('[Federation] Could not save mapping:', e.message); } } async function deleteFederationMapping(roleId) { if (!supabase) return; try { await supabase.from('federation_mappings').delete().eq('role_id', roleId); } catch (e) { console.warn('[Federation] Could not delete mapping:', e.message); } } client.saveFederationMapping = saveFederationMapping; client.deleteFederationMapping = deleteFederationMapping; 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; // ============================================================================= // TIERED ACCESS SYSTEM (Open Bot with Federation Gating) // ============================================================================= // Core AeThex servers - auto-verified for federation const AETHEX_OFFICIAL_GUILDS = [ '373713073594302464', // AeThex | Corporation '515711457946632232', // AeThex (Main) '525971009313046529', // AeThex | Nexus '1245619208805416970', // AeThex | GameForge '1275962459596783686', // AeThex | LABS '1284290638564687925', // AeThex | DevOps '1338564560277344287', // AeThex | Foundation '352519501201539072', // AeThex | Lone Star Studio ]; client.AETHEX_OFFICIAL_GUILDS = AETHEX_OFFICIAL_GUILDS; // Check if a server is federation-verified (cached for performance) const federationCache = new Map(); client.federationCache = federationCache; async function isServerFederationVerified(guildId) { if (AETHEX_OFFICIAL_GUILDS.includes(guildId)) return true; if (federationCache.has(guildId)) { const cached = federationCache.get(guildId); if (Date.now() - cached.timestamp < 5 * 60 * 1000) { return cached.verified; } } if (!supabase) return false; try { const { data } = await supabase .from('federation_servers') .select('verified, tier') .eq('guild_id', guildId) .maybeSingle(); const verified = data?.verified === true; federationCache.set(guildId, { verified, tier: data?.tier || 'free', timestamp: Date.now() }); return verified; } catch (e) { return false; } } client.isServerFederationVerified = isServerFederationVerified; async function getServerTier(guildId) { if (AETHEX_OFFICIAL_GUILDS.includes(guildId)) return 'official'; if (federationCache.has(guildId)) { const cached = federationCache.get(guildId); if (Date.now() - cached.timestamp < 5 * 60 * 1000) { return cached.tier || 'free'; } } if (!supabase) return 'free'; try { const { data } = await supabase .from('federation_servers') .select('verified, tier') .eq('guild_id', guildId) .maybeSingle(); const tier = data?.tier || 'free'; federationCache.set(guildId, { verified: data?.verified, tier, timestamp: Date.now() }); return tier; } catch (e) { return 'free'; } } client.getServerTier = getServerTier; client.on('error', (error) => { console.error('[Discord] Client error:', error.message); addErrorLog('discord', 'Discord client error', { error: error.message }); }); client.on('warn', (warning) => { console.warn('[Discord] Warning:', warning); addErrorLog('warning', 'Discord warning', { warning }); }); client.on('guildCreate', async (guild) => { console.log(`[Guild] Joined new server: ${guild.name} (${guild.id}) - ${guild.memberCount} members`); // Create default server config if (supabase) { try { await supabase.from('server_config').upsert({ guild_id: guild.id, guild_name: guild.name, created_at: new Date().toISOString() }, { onConflict: 'guild_id' }); } catch (e) { console.warn('[Guild] Could not create server config:', e.message); } } // Send welcome message to server owner or first available channel const welcomeEmbed = { color: 0x4A90E2, title: 'šŸ‘‹ Thanks for adding AeThex Bot!', description: `Hello **${guild.name}**! I'm now ready to help your community grow with XP, leveling, music, and moderation features.`, fields: [ { name: 'šŸŽ® Core Features (Available Now)', value: '• XP & Leveling System\n• Music Player\n• Basic Moderation\n• Achievements & Quests\n• Welcome/Goodbye Messages', inline: true }, { name: 'šŸ›”ļø Federation Features', value: '• Cross-server ban sync\n• Reputation network\n• Sentinel protection\n• Premium slots\n\n*Requires verification*', inline: true }, { name: 'šŸš€ Getting Started', value: '1. Use `/help` to see all commands\n2. Use `/config` to set up your server\n3. Use `/federation apply` to join the protection network' }, { name: 'šŸ“– Need Help?', value: '[Documentation](https://aethex.bot/commands) • [Support Server](https://discord.gg/aethex)' } ], footer: { text: 'AeThex Bot • Powering Communities' }, timestamp: new Date().toISOString() }; try { const owner = await guild.fetchOwner(); await owner.send({ embeds: [welcomeEmbed] }); } catch (e) { // Try to find a system channel or first text channel const channel = guild.systemChannel || guild.channels.cache.find( c => c.type === 0 && c.permissionsFor(guild.members.me)?.has('SendMessages') ); if (channel) { try { await channel.send({ embeds: [welcomeEmbed] }); } catch (err) {} } } }); // ============================================================================= // SENTINEL: TICKET TRACKING (New) // ============================================================================= const activeTickets = new Map(); client.activeTickets = activeTickets; async function loadActiveTickets() { if (!supabase) return; try { const { data, error } = await supabase .from('tickets') .select('*') .eq('status', 'open'); if (error) throw error; for (const ticket of data || []) { activeTickets.set(ticket.channel_id, { odId: ticket.id, userId: ticket.user_id, guildId: ticket.guild_id, reason: ticket.reason, createdAt: new Date(ticket.created_at).getTime(), }); } console.log(`[Tickets] Loaded ${activeTickets.size} active tickets from database`); } catch (e) { console.warn('[Tickets] Could not load tickets:', e.message); } } async function saveTicket(channelId, data) { if (!supabase) return; try { await supabase.from('tickets').insert({ channel_id: channelId, user_id: data.userId, guild_id: data.guildId, reason: data.reason, status: 'open', }); } catch (e) { console.warn('[Tickets] Could not save ticket:', e.message); } } async function closeTicket(channelId) { if (!supabase) return; try { await supabase .from('tickets') .update({ status: 'closed', closed_at: new Date().toISOString() }) .eq('channel_id', channelId); } catch (e) { console.warn('[Tickets] Could not close ticket:', e.message); } } client.saveTicket = saveTicket; client.closeTicket = closeTicket; // ============================================================================= // SENTINEL: ALERT SYSTEM (New) // ============================================================================= 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; // ============================================================================= // ACTIVITY FEED SYSTEM (New - Dashboard Real-time) // ============================================================================= const activityFeed = []; const MAX_ACTIVITY_EVENTS = 100; const threatAlerts = []; const MAX_THREAT_ALERTS = 50; const errorLogs = []; const MAX_ERROR_LOGS = 50; const commandQueue = []; const MAX_COMMAND_QUEUE = 100; let lastCpuUsage = process.cpuUsage(); let lastCpuTime = Date.now(); function addErrorLog(type, message, details = {}) { const log = { id: Date.now() + Math.random().toString(36).substr(2, 9), type, message, details, timestamp: new Date().toISOString(), }; errorLogs.unshift(log); if (errorLogs.length > MAX_ERROR_LOGS) { errorLogs.pop(); } return log; } function addToCommandQueue(command, status = 'pending') { const entry = { id: Date.now() + Math.random().toString(36).substr(2, 9), command, status, timestamp: new Date().toISOString(), }; commandQueue.unshift(entry); if (commandQueue.length > MAX_COMMAND_QUEUE) { commandQueue.pop(); } return entry; } function updateCommandQueue(id, status) { const entry = commandQueue.find(e => e.id === id); if (entry) { entry.status = status; entry.completedAt = new Date().toISOString(); } } let currentCpuUsage = 0; function updateCpuUsage() { const now = Date.now(); const cpuUsage = process.cpuUsage(lastCpuUsage); const elapsed = (now - lastCpuTime) * 1000; if (elapsed > 0) { const userPercent = (cpuUsage.user / elapsed) * 100; const systemPercent = (cpuUsage.system / elapsed) * 100; currentCpuUsage = Math.min(100, Math.round(userPercent + systemPercent)); } lastCpuUsage = process.cpuUsage(); lastCpuTime = now; } setInterval(updateCpuUsage, 5000); function getCpuUsage() { return currentCpuUsage; } client.addErrorLog = addErrorLog; client.errorLogs = errorLogs; client.commandQueue = commandQueue; client.addToCommandQueue = addToCommandQueue; client.updateCommandQueue = updateCommandQueue; // Analytics tracking const analyticsData = { commandUsage: {}, xpDistributed: 0, newMembers: 0, modActions: { warnings: 0, kicks: 0, bans: 0, timeouts: 0 }, hourlyActivity: Array(24).fill(0), dailyActivity: Array(7).fill(0), lastReset: Date.now(), }; function trackCommand(commandName) { analyticsData.commandUsage[commandName] = (analyticsData.commandUsage[commandName] || 0) + 1; const hour = new Date().getHours(); const day = new Date().getDay(); analyticsData.hourlyActivity[hour]++; analyticsData.dailyActivity[day]++; } function trackXP(amount) { analyticsData.xpDistributed += amount; } function trackNewMember() { analyticsData.newMembers++; } function trackModAction(type) { if (analyticsData.modActions[type] !== undefined) { analyticsData.modActions[type]++; } } function resetDailyAnalytics() { const now = Date.now(); const lastReset = analyticsData.lastReset; const dayMs = 24 * 60 * 60 * 1000; if (now - lastReset > dayMs) { analyticsData.commandUsage = {}; analyticsData.xpDistributed = 0; analyticsData.newMembers = 0; analyticsData.modActions = { warnings: 0, kicks: 0, bans: 0, timeouts: 0 }; analyticsData.lastReset = now; } } client.trackCommand = trackCommand; client.trackXP = trackXP; client.trackNewMember = trackNewMember; client.trackModAction = trackModAction; client.analyticsData = analyticsData; function addActivity(type, data) { const event = { id: Date.now() + Math.random().toString(36).substr(2, 9), type, data, timestamp: new Date().toISOString(), }; activityFeed.unshift(event); if (activityFeed.length > MAX_ACTIVITY_EVENTS) { activityFeed.pop(); } return event; } function addThreatAlert(level, message, details = {}) { const alert = { id: Date.now() + Math.random().toString(36).substr(2, 9), level, message, details, timestamp: new Date().toISOString(), resolved: false, }; threatAlerts.unshift(alert); if (threatAlerts.length > MAX_THREAT_ALERTS) { threatAlerts.pop(); } return alert; } client.addActivity = addActivity; client.activityFeed = activityFeed; client.addThreatAlert = addThreatAlert; client.threatAlerts = threatAlerts; // ============================================================================= // COMMAND LOADING // ============================================================================= 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}`); } } } // ============================================================================= // EVENT LOADING // ============================================================================= 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}`); } } } // ============================================================================= // SENTINEL LISTENER LOADING (New) // ============================================================================= 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}`); } } } // ============================================================================= // GENERAL LISTENER LOADING (Welcome, Goodbye, XP Tracker) // ============================================================================= const listenersPath = path.join(__dirname, "listeners"); const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js', 'voiceXp.js', 'starboard.js', 'federationProtection.js', 'streamChecker.js']; for (const file of generalListenerFiles) { const filePath = path.join(listenersPath, file); if (fs.existsSync(filePath)) { const listener = require(filePath); if ("name" in listener && "execute" in listener) { // For 'ready' event: no args are passed, so call execute(client, supabase) directly // For other events (messageCreate, etc.): args are passed, then client/supabase if (listener.name === 'ready') { if (listener.once) { client.once(listener.name, () => listener.execute(client, supabase)); } else { client.on(listener.name, () => listener.execute(client, supabase)); } } else { client.on(listener.name, (...args) => listener.execute(...args, client, supabase)); } console.log(`Loaded listener: ${file}`); } } } // ============================================================================= // SERVER CONFIGS MAP // ============================================================================= const serverConfigs = new Map(); client.serverConfigs = serverConfigs; async function loadServerConfigs() { if (!supabase) return; try { const { data, error } = await supabase .from('server_config') .select('*'); if (error) throw error; for (const config of data || []) { serverConfigs.set(config.guild_id, config); } console.log(`[Config] Loaded ${serverConfigs.size} server configurations`); } catch (e) { console.warn('[Config] Could not load server configs:', e.message); } } // ============================================================================= // FEED SYNC SETUP (Modified: Guard for missing Supabase) // ============================================================================= let feedSyncModule = null; let setupFeedListener = null; let sendPostToDiscord = null; let getFeedChannelId = () => null; try { feedSyncModule = require("./listeners/feedSync"); setupFeedListener = feedSyncModule.setupFeedListener; sendPostToDiscord = feedSyncModule.sendPostToDiscord; getFeedChannelId = feedSyncModule.getFeedChannelId; } catch (e) { console.log("Feed sync module not available"); } // ============================================================================= // INTERACTION HANDLER (Modified: Added button handling for tickets) // ============================================================================= client.on("interactionCreate", async (interaction) => { console.log(`[Interaction] Received: type=${interaction.type}, commandName=${interaction.commandName || 'N/A'}, user=${interaction.user?.tag}, guild=${interaction.guildId}`); if (interaction.isChatInputCommand()) { console.log(`[Command] Processing: ${interaction.commandName}`); const command = client.commands.get(interaction.commandName); if (!command) { console.warn(`No command matching ${interaction.commandName} was found.`); return; } const queueEntry = addToCommandQueue(`/${interaction.commandName} by ${interaction.user.tag}`, 'pending'); const startTime = Date.now(); try { console.log(`[Command] Executing: ${interaction.commandName}`); await command.execute(interaction, supabase, client); const executionTime = Date.now() - startTime; console.log(`[Command] Completed: ${interaction.commandName} (${executionTime}ms)`); updateCommandQueue(queueEntry.id, 'completed'); trackCommand(interaction.commandName); resetDailyAnalytics(); // Track command usage for achievements if (supabase && interaction.guildId) { trackCommandForAchievements(interaction.user.id, interaction.guildId, interaction.member, supabase, client).catch(() => {}); } const activityData = { command: interaction.commandName, user: interaction.user.tag, userId: interaction.user.id, guild: interaction.guild?.name || 'DM', guildId: interaction.guildId, executionTime, }; addActivity('command', activityData); // Log to database logCommand({ commandName: interaction.commandName, userId: interaction.user.id, userTag: interaction.user.tag, guildId: interaction.guildId, guildName: interaction.guild?.name || 'DM', channelId: interaction.channelId, success: true, executionTime, }); // Broadcast via WebSocket if (typeof wsBroadcast === 'function') { wsBroadcast('command', activityData); } } catch (error) { const executionTime = Date.now() - startTime; console.error(`Error executing ${interaction.commandName}:`, error); updateCommandQueue(queueEntry.id, 'failed'); addErrorLog('command', `Error in /${interaction.commandName}`, { command: interaction.commandName, user: interaction.user.tag, userId: interaction.user.id, guild: interaction.guild?.name || 'DM', error: error.message, }); // Log failed command to database logCommand({ commandName: interaction.commandName, userId: interaction.user.id, userTag: interaction.user.tag, guildId: interaction.guildId, guildName: interaction.guild?.name || 'DM', channelId: interaction.channelId, success: false, errorMessage: error.message, executionTime, }); try { const errorEmbed = new EmbedBuilder() .setColor(0xff0000) .setTitle("Command Error") .setDescription("There was an error while executing this command.") .setFooter({ text: "Contact support if this persists" }); if (interaction.replied || interaction.deferred) { await interaction.followUp({ embeds: [errorEmbed], ephemeral: true }).catch(() => {}); } else { await interaction.reply({ embeds: [errorEmbed], ephemeral: true }).catch(() => {}); } } catch (replyError) { console.error("Failed to send error response:", replyError.message); } } } if (interaction.isButton()) { const [action, ...params] = interaction.customId.split('_'); if (action === 'ticket') { const ticketAction = params[0]; if (ticketAction === 'close') { try { const channel = interaction.channel; if (channel && channel.type === ChannelType.GuildText) { await interaction.reply({ content: 'Closing ticket...', ephemeral: true }); activeTickets.delete(channel.id); await closeTicket(channel.id); setTimeout(() => channel.delete().catch(console.error), 3000); } } catch (err) { console.error('Ticket close error:', err); } } } if (action === 'role') { const roleId = params.join('_'); try { const role = interaction.guild.roles.cache.get(roleId); if (!role) { return interaction.reply({ content: 'This role no longer exists.', ephemeral: true }); } const member = interaction.member; if (member.roles.cache.has(roleId)) { await member.roles.remove(roleId); await interaction.reply({ content: `Removed ${role} from you!`, ephemeral: true }); } else { await member.roles.add(roleId); await interaction.reply({ content: `Added ${role} to you!`, ephemeral: true }); } } catch (err) { console.error('Role button error:', err); await interaction.reply({ content: 'Failed to toggle role. Check bot permissions.', ephemeral: true }).catch(() => {}); } } if (action === 'giveaway') { const giveawayAction = params[0]; if (giveawayAction === 'enter') { try { const messageId = interaction.message.id; let giveawayData = client.giveaways?.get(messageId); let entries = giveawayData?.entries || []; if (supabase) { const { data } = await supabase .from('giveaways') .select('*') .eq('message_id', messageId) .single(); if (data) { entries = data.entries || []; giveawayData = data; } } if (!giveawayData) { return interaction.reply({ content: 'This giveaway is no longer active.', ephemeral: true }); } if (giveawayData.required_role) { const hasRole = interaction.member.roles.cache.has(giveawayData.required_role); if (!hasRole) { return interaction.reply({ content: `You need the <@&${giveawayData.required_role}> role to enter!`, ephemeral: true }); } } if (entries.includes(interaction.user.id)) { return interaction.reply({ content: 'You have already entered this giveaway!', ephemeral: true }); } entries.push(interaction.user.id); if (client.giveaways?.has(messageId)) { client.giveaways.get(messageId).entries = entries; } if (supabase) { await supabase .from('giveaways') .update({ entries: entries }) .eq('message_id', messageId); } const embed = EmbedBuilder.from(interaction.message.embeds[0]); const entriesField = embed.data.fields?.find(f => f.name.includes('Entries')); if (entriesField) { entriesField.value = `${entries.length}`; } await interaction.message.edit({ embeds: [embed] }); await interaction.reply({ content: `You have entered the giveaway! Total entries: ${entries.length}`, ephemeral: true }); } catch (err) { console.error('Giveaway entry error:', err); await interaction.reply({ content: 'Failed to enter giveaway.', ephemeral: true }).catch(() => {}); } } } if (action === 'backup') { const backupAction = params[0]; if (backupAction === 'restore' || backupAction === 'delete') { const backupId = params[1]; if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) { return interaction.reply({ content: 'Only administrators can manage backups.', ephemeral: true }); } if (backupAction === 'delete') { try { const { data: backup } = await supabase .from('server_backups') .select('name') .eq('id', backupId) .eq('guild_id', interaction.guildId) .single(); if (!backup) { return interaction.reply({ content: 'Backup not found.', ephemeral: true }); } await supabase.from('server_backups').delete().eq('id', backupId); const embed = new EmbedBuilder() .setColor(0x00ff00) .setTitle('Backup Deleted') .setDescription(`Successfully deleted backup: **${backup.name}**`) .setTimestamp(); await interaction.update({ embeds: [embed], components: [] }); } catch (err) { console.error('Backup delete button error:', err); await interaction.reply({ content: 'Failed to delete backup.', ephemeral: true }).catch(() => {}); } } } if (backupAction === 'confirm') { if (params[1] === 'restore') { const backupId = params[2]; const components = params[3] || 'all'; try { await interaction.deferUpdate(); const { data: backup } = await supabase .from('server_backups') .select('*') .eq('id', backupId) .eq('guild_id', interaction.guildId) .single(); if (!backup) { return interaction.editReply({ content: 'Backup not found.', embeds: [], components: [] }); } const { createBackup, performRestore } = require('./commands/backup'); const preRestoreBackup = await createBackup(interaction.guild, supabase, interaction.guildId); await supabase.from('server_backups').insert({ guild_id: interaction.guildId, name: `Pre-restore backup (${new Date().toLocaleDateString()})`, description: 'Automatic backup before restore', backup_type: 'auto', created_by: interaction.user.id, data: preRestoreBackup, roles_count: preRestoreBackup.roles?.length || 0, channels_count: preRestoreBackup.channels?.length || 0, size_bytes: JSON.stringify(preRestoreBackup).length }); const results = await performRestore(interaction.guild, backup.data, components, supabase); const embed = new EmbedBuilder() .setColor(0x00ff00) .setTitle('Restore Complete') .setDescription(`Restored from backup: **${backup.name}**`) .addFields( { name: 'Roles', value: `Created: ${results.roles.created} | Skipped: ${results.roles.skipped} | Failed: ${results.roles.failed}`, inline: false }, { name: 'Channels', value: `Created: ${results.channels.created} | Skipped: ${results.channels.skipped} | Failed: ${results.channels.failed}`, inline: false }, { name: 'Bot Config', value: results.botConfig ? 'Restored' : 'Not restored', inline: true } ) .setFooter({ text: 'A backup was created before this restore' }) .setTimestamp(); await interaction.editReply({ embeds: [embed], components: [] }); } catch (err) { console.error('Backup restore button error:', err); await interaction.editReply({ content: 'Failed to restore backup.', embeds: [], components: [] }).catch(() => {}); } } } if (backupAction === 'cancel') { await interaction.update({ content: 'Restore cancelled.', embeds: [], components: [] }); } } } if (interaction.isModalSubmit()) { if (interaction.customId.startsWith('embed_modal_')) { try { const parts = interaction.customId.split('_'); const channelId = parts[2]; const color = parseInt(parts[3], 16); const title = interaction.fields.getTextInputValue('embed_title'); const description = interaction.fields.getTextInputValue('embed_description'); const imageUrl = interaction.fields.getTextInputValue('embed_image') || null; const thumbnailUrl = interaction.fields.getTextInputValue('embed_thumbnail') || null; const footerText = interaction.fields.getTextInputValue('embed_footer') || null; const embed = new EmbedBuilder() .setColor(color) .setTitle(title) .setDescription(description) .setTimestamp(); if (imageUrl) embed.setImage(imageUrl); if (thumbnailUrl) embed.setThumbnail(thumbnailUrl); if (footerText) embed.setFooter({ text: footerText }); const channel = await client.channels.fetch(channelId); await channel.send({ embeds: [embed] }); await interaction.reply({ content: `Embed sent to <#${channelId}>!`, ephemeral: true }); } catch (err) { console.error('Embed modal error:', err); await interaction.reply({ content: 'Failed to send embed. Check permissions and URLs.', ephemeral: true }).catch(() => {}); } } } if (interaction.isStringSelectMenu()) { if (interaction.customId === 'help_category') { try { const category = interaction.values[0]; const { getCategoryEmbed } = require('./commands/help'); const embed = getCategoryEmbed(category); await interaction.update({ embeds: [embed] }); } catch (err) { console.error('Help select error:', err); } } } }); // ============================================================================= // COMMANDS FOR REGISTRATION (Uses actual command file definitions) // ============================================================================= function getCommandsToRegister() { return Array.from(client.commands.values()).map(cmd => cmd.data.toJSON()); } // ============================================================================= // COMMAND REGISTRATION FUNCTION // ============================================================================= async function registerDiscordCommands() { try { const rest = new REST({ version: "10" }).setToken( process.env.DISCORD_BOT_TOKEN, ); const commandsToRegister = getCommandsToRegister(); console.log( `Registering ${commandsToRegister.length} slash commands...`, ); try { const data = await rest.put( Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), { body: commandsToRegister }, ); console.log(`Successfully registered ${data.length} slash commands`); return { success: true, count: data.length, results: null }; } catch (bulkError) { if (bulkError.code === 50240) { console.warn( "Error 50240: Entry Point detected. Registering individually...", ); const results = []; let successCount = 0; let skipCount = 0; for (const command of commandsToRegister) { try { const posted = await rest.post( Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), { body: command }, ); results.push({ name: command.name, status: "registered", id: posted.id, }); successCount++; } catch (postError) { if (postError.code === 50045) { results.push({ name: command.name, status: "already_exists", }); skipCount++; } else { results.push({ name: command.name, status: "error", error: postError.message, }); } } } console.log( `Registration complete: ${successCount} new, ${skipCount} already existed`, ); return { success: true, count: successCount, skipped: skipCount, results, }; } throw bulkError; } } catch (error) { console.error("Failed to register commands:", error); return { success: false, error: error.message }; } } // ============================================================================= // HTTP SERVER (Modified: Added Sentinel stats to health endpoint) // ============================================================================= // Railway provides PORT - use it for the main web service // Health server uses internal port or falls back to 3000 const healthPort = process.env.HEALTH_PORT || 3000; const ADMIN_TOKEN = process.env.DISCORD_ADMIN_TOKEN || "aethex-bot-admin"; const checkAdminAuth = (req) => { const authHeader = req.headers.authorization; return authHeader === `Bearer ${ADMIN_TOKEN}`; }; const httpServer = http.createServer((req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); res.setHeader("Content-Type", "application/json"); if (req.method === "OPTIONS") { res.writeHead(200); res.end(); return; } if (req.url === "/" || req.url === "/dashboard") { res.setHeader("Content-Type", "text/html"); try { const html = fs.readFileSync(dashboardPath, "utf8"); res.writeHead(200); res.end(html); } catch (e) { res.writeHead(404); res.end("

Dashboard not found

"); } return; } 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, supabaseConnected: !!supabase, 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, federationLinks: federationMappings.size, })); return; } if (req.url === "/activity" || req.url.startsWith("/activity?")) { const url = new URL(req.url, `http://localhost:${healthPort}`); const limit = parseInt(url.searchParams.get('limit') || '50'); const since = url.searchParams.get('since'); let events = activityFeed.slice(0, Math.min(limit, 100)); if (since) { events = events.filter(e => new Date(e.timestamp) > new Date(since)); } res.writeHead(200); res.end(JSON.stringify({ events, total: activityFeed.length, timestamp: new Date().toISOString(), })); return; } if (req.url === "/tickets") { const ticketList = Array.from(activeTickets.entries()).map(([channelId, data]) => ({ channelId, userId: data.userId, guildId: data.guildId, reason: data.reason, createdAt: new Date(data.createdAt).toISOString(), age: Math.floor((Date.now() - data.createdAt) / 60000), })); res.writeHead(200); res.end(JSON.stringify({ tickets: ticketList, count: ticketList.length, timestamp: new Date().toISOString(), })); return; } if (req.url === "/threats") { const unresolvedThreats = threatAlerts.filter(t => !t.resolved); const threatLevel = unresolvedThreats.length > 5 ? 'Critical' : unresolvedThreats.length > 2 ? 'High' : unresolvedThreats.length > 0 ? 'Medium' : 'Low'; res.writeHead(200); res.end(JSON.stringify({ alerts: threatAlerts.slice(0, 50), unresolvedCount: unresolvedThreats.length, threatLevel, heatMapSize: heatMap.size, timestamp: new Date().toISOString(), })); return; } if (req.url === "/server-health") { const guilds = client.guilds.cache.map(g => { const botMember = g.members.cache.get(client.user.id); return { id: g.id, name: g.name, memberCount: g.memberCount, online: true, permissions: botMember?.permissions.has('Administrator') ? 'Admin' : 'Limited', joinedAt: g.joinedAt?.toISOString(), }; }); res.writeHead(200); res.end(JSON.stringify({ guilds, botStatus: client.isReady() ? 'online' : 'offline', ping: client.ws.ping, timestamp: new Date().toISOString(), })); return; } if (req.url === "/system-info") { const memUsage = process.memoryUsage(); const cpu = getCpuUsage(); const pendingCommands = commandQueue.filter(c => c.status === 'pending').length; const completedCommands = commandQueue.filter(c => c.status === 'completed').length; const failedCommands = commandQueue.filter(c => c.status === 'failed').length; res.writeHead(200); res.end(JSON.stringify({ uptime: Math.floor(process.uptime()), memory: { heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024), heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024), rss: Math.round(memUsage.rss / 1024 / 1024), }, cpu: cpu, nodeVersion: process.version, platform: process.platform, ping: client.ws.ping, guilds: client.guilds.cache.size, commands: client.commands.size, activityEvents: activityFeed.length, whitelistedUsers: whitelistedUsers.length, errorLogs: errorLogs.slice(0, 20), errorCount: errorLogs.length, commandQueue: { pending: pendingCommands, completed: completedCommands, failed: failedCommands, total: commandQueue.length, recent: commandQueue.slice(0, 10), }, timestamp: new Date().toISOString(), })); return; } if (req.url === "/analytics") { resetDailyAnalytics(); const commandsArray = Object.entries(analyticsData.commandUsage) .map(([name, count]) => ({ name, count })) .sort((a, b) => b.count - a.count); const totalCommands = commandsArray.reduce((sum, c) => sum + c.count, 0); const totalModActions = Object.values(analyticsData.modActions).reduce((sum, c) => sum + c, 0); res.writeHead(200); res.end(JSON.stringify({ commandsToday: totalCommands, xpDistributed: analyticsData.xpDistributed, newMembers: analyticsData.newMembers, modActionsTotal: totalModActions, modActions: analyticsData.modActions, commandUsage: commandsArray.slice(0, 15), hourlyActivity: analyticsData.hourlyActivity, dailyActivity: analyticsData.dailyActivity, lastReset: new Date(analyticsData.lastReset).toISOString(), timestamp: new Date().toISOString(), })); return; } if (req.url === "/leaderboard") { if (!supabase) { res.writeHead(200); res.end(JSON.stringify({ success: false, message: "Supabase not configured", xpLeaders: [], topChatters: [], topMods: [], })); return; } (async () => { try { const { data: xpLeaders } = await supabase .from('user_profiles') .select('id, username, avatar_url, xp') .order('xp', { ascending: false }) .limit(10); const { data: modActors } = await supabase .from('mod_actions') .select('moderator_id, moderator_tag') .limit(100); const modCounts = {}; (modActors || []).forEach(m => { modCounts[m.moderator_id] = modCounts[m.moderator_id] || { count: 0, tag: m.moderator_tag }; modCounts[m.moderator_id].count++; }); const topMods = Object.entries(modCounts) .map(([id, data]) => ({ id, tag: data.tag, count: data.count })) .sort((a, b) => b.count - a.count) .slice(0, 10); res.writeHead(200); res.end(JSON.stringify({ success: true, xpLeaders: (xpLeaders || []).map((u, i) => ({ rank: i + 1, username: u.username || 'Unknown', avatarUrl: u.avatar_url, xp: u.xp || 0, level: Math.floor(Math.sqrt((u.xp || 0) / 100)), })), topMods, })); } catch (error) { res.writeHead(500); res.end(JSON.stringify({ success: false, error: error.message })); } })(); return; } if (req.url === "/bot-status") { if (!checkAdminAuth(req)) { res.writeHead(401); res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); return; } const channelId = getFeedChannelId(); const guilds = client.guilds.cache.map((guild) => ({ id: guild.id, name: guild.name, memberCount: guild.memberCount, icon: guild.iconURL(), })); res.writeHead(200); res.end( JSON.stringify({ status: client.isReady() ? "online" : "offline", bot: { tag: client.user?.tag || "Not logged in", id: client.user?.id, avatar: client.user?.displayAvatarURL(), }, guilds: guilds, guildCount: client.guilds.cache.size, commands: Array.from(client.commands.keys()), commandCount: client.commands.size, uptime: Math.floor(process.uptime()), feedBridge: { enabled: !!channelId, channelId: channelId, }, sentinel: { heatMapSize: heatMap.size, activeTickets: activeTickets.size, federationMappings: federationMappings.size, }, supabaseConnected: !!supabase, timestamp: new Date().toISOString(), }), ); return; } if (req.url === "/linked-users") { if (!checkAdminAuth(req)) { res.writeHead(401); res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); return; } if (!supabase) { res.writeHead(200); res.end(JSON.stringify({ success: true, links: [], count: 0, message: "Supabase not configured" })); return; } (async () => { try { const { data: links, error } = await supabase .from("discord_links") .select("discord_id, user_id, primary_arm, created_at") .order("created_at", { ascending: false }) .limit(50); if (error) throw error; const enrichedLinks = await Promise.all( (links || []).map(async (link) => { const { data: profile } = await supabase .from("user_profiles") .select("username, avatar_url") .eq("id", link.user_id) .single(); return { discord_id: link.discord_id.slice(0, 6) + "***", user_id: link.user_id.slice(0, 8) + "...", primary_arm: link.primary_arm, created_at: link.created_at, profile: profile ? { username: profile.username, avatar_url: profile.avatar_url, } : null, }; }) ); res.writeHead(200); res.end(JSON.stringify({ success: true, links: enrichedLinks, count: enrichedLinks.length })); } catch (error) { res.writeHead(500); res.end(JSON.stringify({ success: false, error: error.message })); } })(); return; } if (req.url === "/command-stats") { if (!checkAdminAuth(req)) { res.writeHead(401); res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); return; } const cmds = getCommandsToRegister(); const stats = { commands: cmds.map((cmd) => ({ name: cmd.name, description: cmd.description, options: cmd.options?.length || 0, })), totalCommands: cmds.length, }; res.writeHead(200); res.end(JSON.stringify({ success: true, stats })); return; } if (req.url === "/feed-stats") { if (!checkAdminAuth(req)) { res.writeHead(401); res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); return; } if (!supabase) { res.writeHead(200); res.end(JSON.stringify({ success: true, stats: { totalPosts: 0, discordPosts: 0, websitePosts: 0, recentPosts: [] }, message: "Supabase not configured" })); return; } (async () => { try { const { count: totalPosts } = await supabase .from("community_posts") .select("*", { count: "exact", head: true }); const { count: discordPosts } = await supabase .from("community_posts") .select("*", { count: "exact", head: true }) .eq("source", "discord"); const { count: websitePosts } = await supabase .from("community_posts") .select("*", { count: "exact", head: true }) .or("source.is.null,source.neq.discord"); const { data: recentPosts } = await supabase .from("community_posts") .select("id, content, source, created_at") .order("created_at", { ascending: false }) .limit(10); res.writeHead(200); res.end( JSON.stringify({ success: true, stats: { totalPosts: totalPosts || 0, discordPosts: discordPosts || 0, websitePosts: websitePosts || 0, recentPosts: (recentPosts || []).map(p => ({ id: p.id, content: p.content?.slice(0, 100) + (p.content?.length > 100 ? "..." : ""), source: p.source, created_at: p.created_at, })), }, }) ); } catch (error) { res.writeHead(500); res.end(JSON.stringify({ success: false, error: error.message })); } })(); return; } if (req.url === "/send-to-discord" && req.method === "POST") { let body = ""; req.on("data", (chunk) => { body += chunk.toString(); }); req.on("end", async () => { try { const authHeader = req.headers.authorization; const expectedToken = process.env.DISCORD_BRIDGE_TOKEN || "aethex-bridge"; if (authHeader !== `Bearer ${expectedToken}`) { res.writeHead(401); res.end(JSON.stringify({ error: "Unauthorized" })); return; } const post = JSON.parse(body); console.log("[API] Received post to send to Discord:", post.id); if (sendPostToDiscord) { const result = await sendPostToDiscord(post, post.author); res.writeHead(result.success ? 200 : 500); res.end(JSON.stringify(result)); } else { res.writeHead(500); res.end(JSON.stringify({ error: "Feed sync not available" })); } } catch (error) { console.error("[API] Error processing send-to-discord:", error); res.writeHead(500); res.end(JSON.stringify({ error: error.message })); } }); return; } if (req.url === "/bridge-status") { const channelId = getFeedChannelId(); res.writeHead(200); res.end( JSON.stringify({ enabled: !!channelId, channelId: channelId, botReady: client.isReady(), }), ); return; } if (req.url.startsWith("/leave-guild/") && req.method === "POST") { if (!checkAdminAuth(req)) { res.writeHead(401); res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); return; } const guildId = req.url.split("/leave-guild/")[1]; (async () => { try { const guild = client.guilds.cache.get(guildId); if (!guild) { res.writeHead(404); res.end(JSON.stringify({ error: "Guild not found" })); return; } const guildName = guild.name; await guild.leave(); console.log(`[Admin] Left guild: ${guildName} (${guildId})`); res.writeHead(200); res.end(JSON.stringify({ success: true, message: `Left guild: ${guildName}` })); } catch (error) { res.writeHead(500); res.end(JSON.stringify({ error: error.message })); } })(); return; } if (req.url.startsWith("/create-invite/") && req.method === "GET") { if (!checkAdminAuth(req)) { res.writeHead(401); res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); return; } const guildId = req.url.split("/create-invite/")[1]; (async () => { try { const guild = client.guilds.cache.get(guildId); if (!guild) { res.writeHead(404); res.end(JSON.stringify({ error: "Guild not found" })); return; } const channel = guild.channels.cache.find(ch => ch.type === ChannelType.GuildText && ch.permissionsFor(guild.members.me).has('CreateInstantInvite')); if (!channel) { res.writeHead(403); res.end(JSON.stringify({ error: "No channel available to create invite" })); return; } const invite = await channel.createInvite({ maxAge: 86400, maxUses: 1 }); res.writeHead(200); res.end(JSON.stringify({ success: true, invite: invite.url, guild: guild.name, expiresIn: "24 hours" })); } catch (error) { res.writeHead(500); res.end(JSON.stringify({ error: error.message })); } })(); return; } if (req.url === "/register-commands") { if (req.method === "GET") { if (!checkAdminAuth(req)) { res.writeHead(401); res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); return; } res.writeHead(200, { "Content-Type": "text/html" }); res.end(` Register Discord Commands

Discord Commands Registration

Click to register all ${client.commands.size} slash commands

Registering... please wait...
`); return; } if (req.method === "POST") { if (!checkAdminAuth(req)) { res.writeHead(401); res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); return; } registerDiscordCommands().then((result) => { if (result.success) { res.writeHead(200); res.end(JSON.stringify(result)); } else { res.writeHead(500); res.end(JSON.stringify(result)); } }); return; } } // ============================================================================= // MANAGEMENT API ENDPOINTS (Server Config, Whitelist, Roles, Announcements) // ============================================================================= // GET /server-config/:guildId - Get server configuration if (req.url.startsWith("/server-config/") && req.method === "GET") { const guildId = req.url.split("/server-config/")[1].split("?")[0]; if (!guildId) { res.writeHead(400); res.end(JSON.stringify({ error: "Guild ID required" })); return; } const config = serverConfigs.get(guildId) || { guild_id: guildId, welcome_channel: null, goodbye_channel: null, modlog_channel: null, level_up_channel: null, auto_role: null, }; const guild = client.guilds.cache.get(guildId); const channels = guild ? guild.channels.cache .filter(c => c.type === 0) // Text channels .map(c => ({ id: c.id, name: c.name })) : []; const roles = guild ? guild.roles.cache .filter(r => r.name !== '@everyone') .map(r => ({ id: r.id, name: r.name, color: r.hexColor })) : []; res.writeHead(200); res.end(JSON.stringify({ config, channels, roles, guildName: guild?.name || 'Unknown', })); return; } // POST /server-config - Save server configuration if (req.url === "/server-config" && req.method === "POST") { if (!checkAdminAuth(req)) { res.writeHead(401); res.end(JSON.stringify({ error: "Unauthorized" })); return; } let body = ""; req.on("data", chunk => body += chunk); req.on("end", async () => { try { const data = JSON.parse(body); const guildId = data.guild_id; if (!guildId) { res.writeHead(400); res.end(JSON.stringify({ error: "Guild ID required" })); return; } const configData = { guild_id: guildId, welcome_channel: data.welcome_channel || null, goodbye_channel: data.goodbye_channel || null, modlog_channel: data.modlog_channel || null, level_up_channel: data.level_up_channel || null, auto_role: data.auto_role || null, updated_at: new Date().toISOString(), }; serverConfigs.set(guildId, configData); if (supabase) { const { error: dbError } = await supabase.from('server_config').upsert(configData); if (dbError) { console.error('[Config] Failed to save to database:', dbError.message); res.writeHead(500); res.end(JSON.stringify({ error: 'Failed to persist config to database', details: dbError.message })); return; } } res.writeHead(200); res.end(JSON.stringify({ success: true, config: configData, persisted: !!supabase })); } catch (error) { res.writeHead(500); res.end(JSON.stringify({ error: error.message })); } }); return; } // GET /servers - Get official AeThex servers and all connected servers if (req.url === "/whitelist" && req.method === "GET") { const officialServers = AETHEX_OFFICIAL_GUILDS.map(guildId => { const guild = client.guilds.cache.get(guildId); return { id: guildId, name: guild?.name || 'Not Connected', memberCount: guild?.memberCount || 0, connected: !!guild, official: true, }; }); const allServers = client.guilds.cache.map(guild => ({ id: guild.id, name: guild.name, memberCount: guild.memberCount, connected: true, official: AETHEX_OFFICIAL_GUILDS.includes(guild.id), })); res.writeHead(200); res.end(JSON.stringify({ officialServers, allServers, users: whitelistedUsers.map(id => ({ id, note: 'Whitelisted User' })), timestamp: new Date().toISOString(), })); return; } // GET /roles - Get level roles and federation roles if (req.url === "/roles" && req.method === "GET") { (async () => { const levelRoles = []; const fedRoles = Array.from(federationMappings.entries()).map(([roleId, data]) => ({ roleId, name: data.name, guildId: data.guildId, guildName: data.guildName, linkedAt: new Date(data.linkedAt).toISOString(), })); // Try to get level roles from Supabase if (supabase) { try { const { data } = await supabase.from('level_roles').select('*'); if (data) { for (const lr of data) { const guild = client.guilds.cache.get(lr.guild_id); const role = guild?.roles.cache.get(lr.role_id); levelRoles.push({ guildId: lr.guild_id, guildName: guild?.name || 'Unknown', roleId: lr.role_id, roleName: role?.name || 'Unknown', levelRequired: lr.level_required, }); } } } catch (e) { console.warn('Could not fetch level roles:', e.message); } } res.writeHead(200); res.end(JSON.stringify({ levelRoles, federationRoles: fedRoles, timestamp: new Date().toISOString(), })); })(); return; } // POST /announce - Send announcement to servers if (req.url === "/announce" && req.method === "POST") { if (!checkAdminAuth(req)) { res.writeHead(401); res.end(JSON.stringify({ error: "Unauthorized" })); return; } let body = ""; req.on("data", chunk => body += chunk); req.on("end", async () => { try { const { title, message, targets, color } = JSON.parse(body); if (!title || !message) { res.writeHead(400); res.end(JSON.stringify({ error: "Title and message required" })); return; } const embed = new EmbedBuilder() .setTitle(title) .setDescription(message) .setColor(color ? parseInt(color.replace('#', ''), 16) : 0x5865F2) .setTimestamp() .setFooter({ text: 'AeThex Network Announcement' }); const results = []; const targetGuilds = targets && targets.length > 0 ? targets : Array.from(client.guilds.cache.keys()); for (const guildId of targetGuilds) { const guild = client.guilds.cache.get(guildId); if (!guild) { results.push({ guildId, success: false, error: 'Guild not found' }); continue; } try { const config = serverConfigs.get(guildId); let channel = null; if (config?.welcome_channel) { channel = guild.channels.cache.get(config.welcome_channel); } if (!channel) { channel = guild.systemChannel || guild.channels.cache.find(c => c.type === 0 && c.permissionsFor(guild.members.me).has('SendMessages')); } if (channel) { await channel.send({ embeds: [embed] }); results.push({ guildId, guildName: guild.name, success: true }); } else { results.push({ guildId, guildName: guild.name, success: false, error: 'No suitable channel' }); } } catch (error) { results.push({ guildId, guildName: guild?.name, success: false, error: error.message }); } } addActivity('announcement', { title, targetCount: targetGuilds.length, successCount: results.filter(r => r.success).length }); res.writeHead(200); res.end(JSON.stringify({ success: true, results })); } catch (error) { res.writeHead(500); res.end(JSON.stringify({ error: error.message })); } }); return; } // GET /servers - List all connected servers (for dropdowns) if (req.url === "/servers" && req.method === "GET") { const servers = client.guilds.cache.map(g => ({ id: g.id, name: g.name, icon: g.iconURL({ size: 64 }), memberCount: g.memberCount, })); res.writeHead(200); res.end(JSON.stringify({ servers })); return; } // GET /user-lookup/:query - Search for a user by ID or username if (req.url.startsWith("/user-lookup/") && req.method === "GET") { const query = decodeURIComponent(req.url.split("/user-lookup/")[1].split("?")[0]); if (!query || query.length < 2) { res.writeHead(400); res.end(JSON.stringify({ error: "Query too short" })); return; } (async () => { try { const results = []; const isNumericId = /^\d{17,19}$/.test(query); for (const guild of client.guilds.cache.values()) { try { if (isNumericId) { const member = await guild.members.fetch(query).catch(() => null); if (member) { const existingIdx = results.findIndex(r => r.id === member.id); if (existingIdx === -1) { results.push({ id: member.id, tag: member.user.tag, username: member.user.username, displayName: member.displayName, avatar: member.user.displayAvatarURL({ size: 128 }), bot: member.user.bot, createdAt: member.user.createdAt.toISOString(), joinedAt: member.joinedAt?.toISOString(), roles: member.roles.cache.filter(r => r.name !== '@everyone').map(r => ({ id: r.id, name: r.name, color: r.hexColor })).slice(0, 10), servers: [{ id: guild.id, name: guild.name }], heat: getHeat(member.id), }); } else { results[existingIdx].servers.push({ id: guild.id, name: guild.name }); } } } else { const members = await guild.members.fetch({ query, limit: 10 }).catch(() => new Map()); for (const member of members.values()) { const existingIdx = results.findIndex(r => r.id === member.id); if (existingIdx === -1) { results.push({ id: member.id, tag: member.user.tag, username: member.user.username, displayName: member.displayName, avatar: member.user.displayAvatarURL({ size: 128 }), bot: member.user.bot, createdAt: member.user.createdAt.toISOString(), joinedAt: member.joinedAt?.toISOString(), roles: member.roles.cache.filter(r => r.name !== '@everyone').map(r => ({ id: r.id, name: r.name, color: r.hexColor })).slice(0, 10), servers: [{ id: guild.id, name: guild.name }], heat: getHeat(member.id), }); } else { results[existingIdx].servers.push({ id: guild.id, name: guild.name }); } } } } catch (e) {} } // Fetch Supabase profile data if available if (supabase && results.length > 0) { for (const user of results) { try { const { data: link } = await supabase .from('discord_links') .select('user_id, primary_arm') .eq('discord_id', user.id) .single(); if (link) { user.linked = true; user.realm = link.primary_arm; const { data: profile } = await supabase .from('user_profiles') .select('username, xp, daily_streak, badges') .eq('id', link.user_id) .single(); if (profile) { user.aethexUsername = profile.username; user.xp = profile.xp || 0; user.level = Math.floor(Math.sqrt((profile.xp || 0) / 100)); user.dailyStreak = profile.daily_streak || 0; user.badges = profile.badges || []; } } const { data: warnings } = await supabase .from('warnings') .select('id, reason, created_at') .eq('user_id', user.id) .order('created_at', { ascending: false }) .limit(5); user.warnings = warnings || []; const { data: modActions } = await supabase .from('mod_actions') .select('action, reason, created_at') .eq('user_id', user.id) .order('created_at', { ascending: false }) .limit(5); user.modHistory = modActions || []; } catch (e) {} } } res.writeHead(200); res.end(JSON.stringify({ results: results.slice(0, 20), count: results.length, query, timestamp: new Date().toISOString(), })); } catch (error) { res.writeHead(500); res.end(JSON.stringify({ error: error.message })); } })(); return; } // GET /mod-actions - Get recent moderation actions if (req.url === "/mod-actions" && req.method === "GET") { if (!supabase) { res.writeHead(200); res.end(JSON.stringify({ success: false, message: "Supabase not configured", actions: [] })); return; } (async () => { try { const { data: actions, error } = await supabase .from('mod_actions') .select('*') .order('created_at', { ascending: false }) .limit(50); if (error) throw error; const { count: warnCount } = await supabase.from('warnings').select('*', { count: 'exact', head: true }); const { count: banCount } = await supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('action', 'ban'); const { count: kickCount } = await supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('action', 'kick'); const { count: timeoutCount } = await supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('action', 'timeout'); res.writeHead(200); res.end(JSON.stringify({ success: true, actions: actions || [], counts: { warnings: warnCount || 0, bans: banCount || 0, kicks: kickCount || 0, timeouts: timeoutCount || 0, }, timestamp: new Date().toISOString(), })); } catch (error) { res.writeHead(500); res.end(JSON.stringify({ success: false, error: error.message })); } })(); return; } // GET /studio-feed - Get AeThex Studio community feed if (req.url === "/studio-feed" && req.method === "GET") { if (!supabase) { res.writeHead(200); res.end(JSON.stringify({ success: false, message: "Supabase not configured - Studio feed unavailable", posts: [] })); return; } (async () => { try { const { data: posts, error } = await supabase .from('community_posts') .select('*') .order('created_at', { ascending: false }) .limit(20); if (error) throw error; res.writeHead(200); res.end(JSON.stringify({ success: true, posts: posts || [], count: posts?.length || 0, timestamp: new Date().toISOString(), })); } catch (error) { res.writeHead(200); res.end(JSON.stringify({ success: false, message: "No community posts table available", posts: [] })); } })(); return; } // GET /foundation-stats - Get AeThex Foundation statistics if (req.url === "/foundation-stats" && req.method === "GET") { (async () => { const foundationGuild = client.guilds.cache.get(REALM_GUILDS.foundation); const stats = { success: true, contributors: 0, projects: 0, commits: 0, members: foundationGuild?.memberCount || 0, activity: [], timestamp: new Date().toISOString(), }; if (supabase) { try { const { count: contributorCount } = await supabase .from('user_profiles') .select('*', { count: 'exact', head: true }) .not('foundation_contributions', 'is', null); stats.contributors = contributorCount || 0; const { data: activity } = await supabase .from('foundation_activity') .select('*') .order('created_at', { ascending: false }) .limit(10); if (activity) { stats.activity = activity.map(a => ({ type: a.type || 'commit', message: a.message || a.description, author: a.author || a.username, date: a.created_at, })); } } catch (e) { // Tables may not exist, use defaults } } // Estimate projects from federation mappings stats.projects = federationMappings.size || 5; stats.commits = Math.floor(Math.random() * 50) + 100; // Placeholder res.writeHead(200); res.end(JSON.stringify(stats)); })(); return; } // POST /test-webhook - Test a Discord webhook if (req.url === "/test-webhook" && req.method === "POST") { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const { url, message, username } = JSON.parse(body); if (!url || !url.includes('discord.com/api/webhooks')) { res.writeHead(400); res.end(JSON.stringify({ success: false, error: 'Invalid webhook URL' })); return; } if (!message) { res.writeHead(400); res.end(JSON.stringify({ success: false, error: 'Message is required' })); return; } const webhookPayload = { content: message, username: username || 'AeThex Dashboard Test', }; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(webhookPayload), }); if (response.ok || response.status === 204) { res.writeHead(200); res.end(JSON.stringify({ success: true, message: 'Webhook test sent successfully' })); } else { const errorText = await response.text(); res.writeHead(200); res.end(JSON.stringify({ success: false, error: `Webhook failed: ${response.status} - ${errorText}` })); } } catch (error) { res.writeHead(500); res.end(JSON.stringify({ success: false, error: error.message })); } }); return; } // Analytics endpoint with detailed command data if (req.url === "/command-analytics" || req.url.startsWith("/command-analytics?")) { (async () => { try { const url = new URL(req.url, `http://localhost:${healthPort}`); const days = parseInt(url.searchParams.get('days') || '7'); const analytics = await getCommandAnalytics(days); const totalCount = await getTotalCommandCount(); res.writeHead(200); res.end(JSON.stringify({ success: true, totalCommands: totalCount, analytics, timestamp: new Date().toISOString(), })); } catch (error) { res.writeHead(500); res.end(JSON.stringify({ success: false, error: error.message })); } })(); return; } res.writeHead(404); res.end(JSON.stringify({ error: "Not found" })); }); // ============================================================================= // WEBSOCKET SERVER FOR REAL-TIME UPDATES // ============================================================================= const wsClients = new Set(); const wss = new WebSocket.Server({ noServer: true }); wss.on('connection', (ws) => { wsClients.add(ws); console.log(`[WebSocket] Client connected. Total: ${wsClients.size}`); ws.send(JSON.stringify({ type: 'init', data: { status: 'online', guilds: client.guilds.cache.size, commands: client.commands?.size || 0, uptime: Math.floor(process.uptime()), heatMapSize: heatMap.size, activeTickets: activeTickets.size, federationLinks: federationMappings.size, } })); ws.on('close', () => { wsClients.delete(ws); console.log(`[WebSocket] Client disconnected. Total: ${wsClients.size}`); }); ws.on('error', (err) => { console.error('[WebSocket] Error:', err.message); wsClients.delete(ws); }); }); function wsBroadcast(type, data) { const message = JSON.stringify({ type, data, timestamp: new Date().toISOString() }); for (const wsClient of wsClients) { if (wsClient.readyState === WebSocket.OPEN) { try { wsClient.send(message); } catch (err) { console.error('[WebSocket] Broadcast error:', err.message); } } } } setInterval(() => { if (wsClients.size > 0) { wsBroadcast('stats', { guilds: client.guilds.cache.size, commands: client.commands?.size || 0, uptime: Math.floor(process.uptime()), heatMapSize: heatMap.size, activeTickets: activeTickets.size, federationLinks: federationMappings.size, memory: process.memoryUsage(), cpu: getCpuUsage(), }); } }, 5000); // Add WebSocket upgrade handling httpServer.on('upgrade', (request, socket, head) => { wss.handleUpgrade(request, socket, head, (ws) => { wss.emit('connection', ws, request); }); }); httpServer.listen(healthPort, () => { console.log(`Health/API server running on port ${healthPort}`); console.log(`WebSocket server available at ws://localhost:${healthPort}`); console.log(`Register commands at: POST http://localhost:${healthPort}/register-commands`); }); // ============================================================================= // EXPRESS WEB PORTAL (Discord OAuth, User Dashboard, API) // ============================================================================= // Use Railway's PORT for main web service, default to 8080 const webPort = process.env.PORT || 8080; const expressApp = createWebServer(client, supabase); const webServer = http.createServer(expressApp); webServer.listen(webPort, '0.0.0.0', () => { console.log(`Web portal running on port ${webPort}`); console.log(`Dashboard: http://localhost:${webPort}/dashboard`); console.log(`Landing page: http://localhost:${webPort}/`); }); // ============================================================================= // BOT LOGIN AND READY // ============================================================================= client.login(token).catch((error) => { console.error("Failed to login to Discord"); console.error(`Error Code: ${error.code}`); console.error(`Error Message: ${error.message}`); if (error.code === "TokenInvalid") { console.error("\nDISCORD_BOT_TOKEN is invalid!"); console.error("Get a new token from: https://discord.com/developers/applications"); } process.exit(1); }); // ============================================================================= // BOT STATUS - WATCHING PROTECTING THE FEDERATION // ============================================================================= function setWardenStatus(client) { try { client.user.setPresence({ activities: [{ name: 'Protecting the Federation', type: 3 // ActivityType.Watching }], status: 'online' }); console.log('[Status] Set to: WATCHING Protecting the Federation'); } catch (e) { console.error('[Status] Error setting status:', e.message); } } client.once("clientReady", async () => { console.log(`Bot logged in as ${client.user.tag}`); console.log(`Bot ID: ${client.user.id}`); console.log(`CLIENT_ID from env: ${process.env.DISCORD_CLIENT_ID}`); console.log(`IDs match: ${client.user.id === process.env.DISCORD_CLIENT_ID}`); console.log(`Watching ${client.guilds.cache.size} server(s)`); // Load persisted data from Supabase await loadFederationMappings(); await loadActiveTickets(); await loadServerConfigs(); // Auto-register commands on startup console.log("Registering slash commands with Discord..."); const regResult = await registerDiscordCommands(); if (regResult.success) { console.log(`Successfully registered ${regResult.count} commands`); } else { console.error("Failed to register commands:", regResult.error); } // Static status: WATCHING Protecting the Federation setWardenStatus(client); if (setupFeedListener && supabase) { setupFeedListener(client); } sendAlert(`Warden is now online! Watching ${client.guilds.cache.size} servers.`); // Start automatic backup scheduler if (supabase) { startAutoBackupScheduler(client, supabase); startFederationTrustEvaluator(client, supabase); } }); // ============================================================================= // AUTOMATIC BACKUP SCHEDULER // ============================================================================= async function startAutoBackupScheduler(discordClient, supabaseClient) { console.log('[Backup] Starting automatic backup scheduler...'); // Check every hour for servers that need backups setInterval(async () => { try { const { data: settings } = await supabaseClient .from('backup_settings') .select('*') .eq('auto_enabled', true); if (!settings || settings.length === 0) return; for (const setting of settings) { const guild = discordClient.guilds.cache.get(setting.guild_id); if (!guild) continue; // Check if backup is needed const { data: lastBackup } = await supabaseClient .from('server_backups') .select('created_at') .eq('guild_id', setting.guild_id) .eq('backup_type', 'auto') .order('created_at', { ascending: false }) .limit(1) .single(); const intervalMs = (setting.interval_hours || 24) * 60 * 60 * 1000; const lastBackupTime = lastBackup ? new Date(lastBackup.created_at).getTime() : 0; const now = Date.now(); if (now - lastBackupTime >= intervalMs) { console.log(`[Backup] Creating auto backup for guild ${guild.name}`); const { createBackup } = require('./commands/backup'); const backupData = await createBackup(guild, supabaseClient, setting.guild_id); await supabaseClient.from('server_backups').insert({ guild_id: setting.guild_id, name: `Auto Backup - ${new Date().toLocaleDateString()}`, description: 'Scheduled automatic backup', backup_type: 'auto', created_by: null, data: backupData, roles_count: backupData.roles?.length || 0, channels_count: backupData.channels?.length || 0, size_bytes: JSON.stringify(backupData).length }); // Clean up old backups const maxBackups = setting.max_backups || 7; const { data: allBackups } = await supabaseClient .from('server_backups') .select('id') .eq('guild_id', setting.guild_id) .eq('backup_type', 'auto') .order('created_at', { ascending: false }); if (allBackups && allBackups.length > maxBackups) { const toDelete = allBackups.slice(maxBackups).map(b => b.id); await supabaseClient.from('server_backups').delete().in('id', toDelete); console.log(`[Backup] Cleaned up ${toDelete.length} old backups for ${guild.name}`); } console.log(`[Backup] Auto backup complete for ${guild.name}`); } } } catch (error) { console.error('[Backup] Auto backup error:', error.message); } }, 60 * 60 * 1000); // Check every hour console.log('[Backup] Scheduler started - checking every hour'); } // ============================================================================= // FEDERATION TRUST LEVEL PROGRESSION SCHEDULER // ============================================================================= async function startFederationTrustEvaluator(discordClient, supabaseClient) { console.log('[Federation] Starting trust level evaluation scheduler...'); const { calculateTrustLevel, getTrustLevelInfo } = require('./utils/trustLevels'); // Evaluate trust levels daily setInterval(async () => { try { const { data: servers } = await supabaseClient .from('federation_servers') .select('*') .eq('status', 'approved'); if (!servers || servers.length === 0) return; let upgrades = 0; let downgrades = 0; for (const server of servers) { // Get current member count from Discord const guild = discordClient.guilds.cache.get(server.guild_id); const memberCount = guild?.memberCount || server.member_count || 0; // Update member count in database if (guild && memberCount !== server.member_count) { await supabaseClient .from('federation_servers') .update({ member_count: memberCount }) .eq('guild_id', server.guild_id); } // Calculate new trust level const serverData = { ...server, member_count: memberCount }; const newLevel = calculateTrustLevel(serverData); const currentLevel = server.trust_level || 'bronze'; if (newLevel !== currentLevel) { const oldInfo = getTrustLevelInfo(currentLevel); const newInfo = getTrustLevelInfo(newLevel); await supabaseClient .from('federation_servers') .update({ trust_level: newLevel, updated_at: new Date().toISOString(), last_activity: new Date().toISOString() }) .eq('guild_id', server.guild_id); // Determine if upgrade or downgrade const levels = ['bronze', 'silver', 'gold', 'platinum']; const isUpgrade = levels.indexOf(newLevel) > levels.indexOf(currentLevel); if (isUpgrade) { upgrades++; console.log(`[Federation] ${server.guild_name}: ${oldInfo.emoji} ${oldInfo.name} → ${newInfo.emoji} ${newInfo.name} (UPGRADE)`); } else { downgrades++; console.log(`[Federation] ${server.guild_name}: ${oldInfo.emoji} ${oldInfo.name} → ${newInfo.emoji} ${newInfo.name} (DOWNGRADE)`); } // Send notification to the server if we can if (guild) { try { const systemChannel = guild.systemChannel; if (systemChannel) { const { EmbedBuilder } = require('discord.js'); const embed = new EmbedBuilder() .setColor(newInfo.color) .setTitle(`${newInfo.emoji} Federation Trust Level ${isUpgrade ? 'Upgrade' : 'Change'}!`) .setDescription(isUpgrade ? `Congratulations! Your server has been promoted to **${newInfo.name}** tier in the Federation.` : `Your server's Federation tier has been adjusted to **${newInfo.name}**.`) .addFields( { name: 'Previous Tier', value: `${oldInfo.emoji} ${oldInfo.name}`, inline: true }, { name: 'New Tier', value: `${newInfo.emoji} ${newInfo.name}`, inline: true } ) .setTimestamp(); await systemChannel.send({ embeds: [embed] }).catch(() => {}); } } catch (e) { // Silent fail for notifications } } } } if (upgrades > 0 || downgrades > 0) { console.log(`[Federation] Trust evaluation complete: ${upgrades} upgrades, ${downgrades} downgrades`); } } catch (error) { console.error('[Federation] Trust evaluation error:', error.message); } }, 24 * 60 * 60 * 1000); // Evaluate daily // Also run immediately on startup (after a short delay) setTimeout(async () => { console.log('[Federation] Running initial trust level evaluation...'); try { const { data: servers } = await supabaseClient .from('federation_servers') .select('*') .eq('status', 'approved'); console.log(`[Federation] Monitoring ${servers?.length || 0} federation servers`); } catch (e) { console.error('[Federation] Initial check error:', e.message); } }, 10000); console.log('[Federation] Trust evaluator started - checking daily'); } // ============================================================================= // ERROR HANDLING // ============================================================================= 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;