AeThex-Bot-Master/aethex-bot/bot.js
sirpiglr bc8a04825b Add command logging and real-time dashboard updates
Integrates PostgreSQL for command logging and analytics, adds WebSocket support for real-time dashboard updates, and includes `pg` and `ws` dependencies.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 2d4b80d0-ca85-407f-99c3-f3596476f525
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/4ZEVdt6
Replit-Helium-Checkpoint-Created: true
2025-12-08 06:35:49 +00:00

2368 lines
77 KiB
JavaScript

const {
Client,
GatewayIntentBits,
REST,
Routes,
Collection,
EmbedBuilder,
ChannelType,
PermissionFlagsBits,
} = require("discord.js");
const { createClient } = require("@supabase/supabase-js");
const http = require("http");
const fs = require("fs");
const path = require("path");
const { Pool } = require("pg");
const WebSocket = require("ws");
// Dashboard HTML path
const dashboardPath = path.join(__dirname, "public", "dashboard.html");
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,
],
});
// =============================================================================
// 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");
}
// =============================================================================
// POSTGRESQL DATABASE SETUP (Local database for analytics & config)
// =============================================================================
let pgPool = null;
if (process.env.DATABASE_URL) {
pgPool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
pgPool.on('error', (err) => {
console.error('PostgreSQL pool error:', err.message);
});
console.log("PostgreSQL connected");
} else {
console.log("PostgreSQL not configured - using in-memory storage");
}
// =============================================================================
// COMMAND LOGGING SYSTEM
// =============================================================================
async function logCommand(data) {
if (!pgPool) return;
try {
await pgPool.query(
`INSERT INTO command_logs (command_name, user_id, user_tag, guild_id, guild_name, channel_id, success, error_message, execution_time_ms)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
data.commandName,
data.userId,
data.userTag,
data.guildId,
data.guildName,
data.channelId,
data.success,
data.errorMessage || null,
data.executionTime || null
]
);
} catch (err) {
console.error('Failed to log command:', err.message);
}
}
async function getCommandAnalytics(days = 7) {
if (!pgPool) return { commands: [], hourly: [], daily: [], topUsers: [] };
try {
const commandsResult = await pgPool.query(
`SELECT command_name, COUNT(*) as count,
SUM(CASE WHEN success THEN 1 ELSE 0 END) as success_count,
AVG(execution_time_ms) as avg_time
FROM command_logs
WHERE created_at > NOW() - INTERVAL '${days} days'
GROUP BY command_name
ORDER BY count DESC
LIMIT 20`
);
const hourlyResult = await pgPool.query(
`SELECT EXTRACT(HOUR FROM created_at) as hour, COUNT(*) as count
FROM command_logs
WHERE created_at > NOW() - INTERVAL '24 hours'
GROUP BY hour
ORDER BY hour`
);
const dailyResult = await pgPool.query(
`SELECT DATE(created_at) as date, COUNT(*) as count
FROM command_logs
WHERE created_at > NOW() - INTERVAL '${days} days'
GROUP BY date
ORDER BY date`
);
const topUsersResult = await pgPool.query(
`SELECT user_id, user_tag, COUNT(*) as command_count
FROM command_logs
WHERE created_at > NOW() - INTERVAL '${days} days'
GROUP BY user_id, user_tag
ORDER BY command_count DESC
LIMIT 10`
);
return {
commands: commandsResult.rows,
hourly: hourlyResult.rows,
daily: dailyResult.rows,
topUsers: topUsersResult.rows
};
} catch (err) {
console.error('Failed to get command analytics:', err.message);
return { commands: [], hourly: [], daily: [], topUsers: [] };
}
}
async function getTotalCommandCount() {
if (!pgPool) return 0;
try {
const result = await pgPool.query('SELECT COUNT(*) as count FROM command_logs');
return parseInt(result.rows[0].count) || 0;
} catch (err) {
return 0;
}
}
// PostgreSQL-based server config functions
async function saveServerConfigToDB(guildId, config) {
if (!pgPool) return false;
try {
await pgPool.query(
`INSERT INTO server_config (guild_id, welcome_channel, goodbye_channel, modlog_channel, level_up_channel, auto_role, verified_role, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
ON CONFLICT (guild_id) DO UPDATE SET
welcome_channel = EXCLUDED.welcome_channel,
goodbye_channel = EXCLUDED.goodbye_channel,
modlog_channel = EXCLUDED.modlog_channel,
level_up_channel = EXCLUDED.level_up_channel,
auto_role = EXCLUDED.auto_role,
verified_role = EXCLUDED.verified_role,
updated_at = NOW()`,
[guildId, config.welcome_channel, config.goodbye_channel, config.modlog_channel,
config.level_up_channel, config.auto_role, config.verified_role]
);
return true;
} catch (err) {
console.error('Failed to save server config:', err.message);
return false;
}
}
async function getServerConfigFromDB(guildId) {
if (!pgPool) return null;
try {
const result = await pgPool.query(
'SELECT * FROM server_config WHERE guild_id = $1',
[guildId]
);
return result.rows[0] || null;
} catch (err) {
console.error('Failed to get server config:', err.message);
return null;
}
}
// Federation mappings with PostgreSQL
async function saveFederationMappingToDB(guildId, roleId, roleName) {
if (!pgPool) return false;
try {
await pgPool.query(
`INSERT INTO federation_mappings (guild_id, role_id, role_name, linked_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (guild_id, role_id) DO UPDATE SET role_name = EXCLUDED.role_name`,
[guildId, roleId, roleName]
);
return true;
} catch (err) {
console.error('Failed to save federation mapping:', err.message);
return false;
}
}
async function getFederationMappingsFromDB() {
if (!pgPool) return [];
try {
const result = await pgPool.query('SELECT * FROM federation_mappings ORDER BY linked_at DESC');
return result.rows;
} 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
...(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'];
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) {
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();
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);
}
}
}
}
});
// =============================================================================
// 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 check 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`);
});
// =============================================================================
// 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);
});
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);
}
client.user.setActivity("Protecting the Federation", { type: 3 });
if (setupFeedListener && supabase) {
setupFeedListener(client);
}
sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`);
});
// =============================================================================
// 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;