AeThex-Bot-Master/aethex-bot/bot.js
sirpiglr 9b1b8852d0 Fix stream checker error by correctly passing Supabase client
Modify listener registration in bot.js to ensure the Supabase client is correctly passed to the stream checker, resolving the "supabase.from is not a function" error by addressing argument handling for the 'ready' event.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 15e978ac-af85-4dc5-a34f-c363c14b4cba
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/3tJ1Z1J
Replit-Helium-Checkpoint-Created: true
2025-12-13 09:42:47 +00:00

2879 lines
96 KiB
JavaScript

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;
// =============================================================================
// GUILD WHITELIST SYSTEM
// =============================================================================
const WHITELISTED_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
...(process.env.EXTRA_WHITELISTED_GUILDS || '').split(',').filter(Boolean),
];
client.WHITELISTED_GUILDS = WHITELISTED_GUILDS;
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) => {
if (!WHITELISTED_GUILDS.includes(guild.id)) {
console.log(`[Whitelist] Unauthorized server detected: ${guild.name} (${guild.id}) - Leaving...`);
try {
const owner = await guild.fetchOwner();
await owner.send(`Your server "${guild.name}" is not authorized to use AeThex Bot. The bot has automatically left. Contact the AeThex team if you believe this is an error.`).catch(() => {});
} catch (e) {}
await guild.leave();
console.log(`[Whitelist] Left unauthorized server: ${guild.name}`);
return;
}
console.log(`[Whitelist] Joined authorized server: ${guild.name} (${guild.id})`);
});
// =============================================================================
// 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)
// =============================================================================
const healthPort = process.env.HEALTH_PORT || 8080;
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("<h1>Dashboard not found</h1>");
}
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(`
<!DOCTYPE html>
<html>
<head>
<title>Register Discord Commands</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
text-align: center;
max-width: 500px;
}
h1 { color: #333; margin-bottom: 20px; }
p { color: #666; margin-bottom: 30px; }
button {
background: #667eea;
color: white;
border: none;
padding: 12px 30px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: background 0.3s;
}
button:hover { background: #764ba2; }
button:disabled { background: #ccc; cursor: not-allowed; }
#result { margin-top: 30px; padding: 20px; border-radius: 5px; display: none; }
#result.success { background: #d4edda; color: #155724; display: block; }
#result.error { background: #f8d7da; color: #721c24; display: block; }
#loading { display: none; color: #667eea; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<h1>Discord Commands Registration</h1>
<p>Click to register all ${client.commands.size} slash commands</p>
<button id="registerBtn" onclick="registerCommands()">Register Commands</button>
<div id="loading">Registering... please wait...</div>
<div id="result"></div>
</div>
<script>
async function registerCommands() {
const btn = document.getElementById('registerBtn');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
btn.disabled = true;
loading.style.display = 'block';
result.style.display = 'none';
try {
const response = await fetch('/register-commands', {
method: 'POST',
headers: { 'Authorization': 'Bearer ${ADMIN_TOKEN}', 'Content-Type': 'application/json' }
});
const data = await response.json();
loading.style.display = 'none';
if (response.ok && data.success) {
result.className = 'success';
result.innerHTML = '<h3>Success!</h3><p>Registered ' + data.count + ' commands</p>';
} else {
result.className = 'error';
result.innerHTML = '<h3>Error</h3><p>' + (data.error || 'Failed') + '</p>';
}
} catch (error) {
loading.style.display = 'none';
result.className = 'error';
result.innerHTML = '<h3>Error</h3><p>' + error.message + '</p>';
} finally {
btn.disabled = false;
}
}
</script>
</body>
</html>
`);
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 /whitelist - Get whitelisted servers and users
if (req.url === "/whitelist" && req.method === "GET") {
const whitelistedServers = WHITELISTED_GUILDS.map(guildId => {
const guild = client.guilds.cache.get(guildId);
return {
id: guildId,
name: guild?.name || 'Not Connected',
memberCount: guild?.memberCount || 0,
connected: !!guild,
};
});
res.writeHead(200);
res.end(JSON.stringify({
servers: whitelistedServers,
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)
// =============================================================================
const webPort = process.env.PORT || 5000;
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);
}
});
// =============================================================================
// 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');
}
// =============================================================================
// 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;