Introduces a tiered access system for bot features, distinguishing between free, federation (verified), and premium tiers. Updates `federationProtection.js` to check server verification status and `bot.js` to handle Aethex official guilds and server tier checks. Modifies `replit.md` to reflect the new tiered access model and access requirements. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 6a0de952-f48b-480a-b732-18d6f0f9f79a Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/W0QYEBv Replit-Helium-Checkpoint-Created: true
3114 lines
104 KiB
JavaScript
3114 lines
104 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;
|
|
|
|
// =============================================================================
|
|
// TIERED ACCESS SYSTEM (Open Bot with Federation Gating)
|
|
// =============================================================================
|
|
|
|
// Core AeThex servers - auto-verified for federation
|
|
const AETHEX_OFFICIAL_GUILDS = [
|
|
'373713073594302464', // AeThex | Corporation
|
|
'515711457946632232', // AeThex (Main)
|
|
'525971009313046529', // AeThex | Nexus
|
|
'1245619208805416970', // AeThex | GameForge
|
|
'1275962459596783686', // AeThex | LABS
|
|
'1284290638564687925', // AeThex | DevOps
|
|
'1338564560277344287', // AeThex | Foundation
|
|
'352519501201539072', // AeThex | Lone Star Studio
|
|
];
|
|
client.AETHEX_OFFICIAL_GUILDS = AETHEX_OFFICIAL_GUILDS;
|
|
|
|
// Check if a server is federation-verified (cached for performance)
|
|
const federationCache = new Map();
|
|
client.federationCache = federationCache;
|
|
|
|
async function isServerFederationVerified(guildId) {
|
|
if (AETHEX_OFFICIAL_GUILDS.includes(guildId)) return true;
|
|
|
|
if (federationCache.has(guildId)) {
|
|
const cached = federationCache.get(guildId);
|
|
if (Date.now() - cached.timestamp < 5 * 60 * 1000) {
|
|
return cached.verified;
|
|
}
|
|
}
|
|
|
|
if (!supabase) return false;
|
|
|
|
try {
|
|
const { data } = await supabase
|
|
.from('federation_servers')
|
|
.select('verified, tier')
|
|
.eq('guild_id', guildId)
|
|
.maybeSingle();
|
|
|
|
const verified = data?.verified === true;
|
|
federationCache.set(guildId, { verified, tier: data?.tier || 'free', timestamp: Date.now() });
|
|
return verified;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
client.isServerFederationVerified = isServerFederationVerified;
|
|
|
|
async function getServerTier(guildId) {
|
|
if (AETHEX_OFFICIAL_GUILDS.includes(guildId)) return 'official';
|
|
|
|
if (federationCache.has(guildId)) {
|
|
const cached = federationCache.get(guildId);
|
|
if (Date.now() - cached.timestamp < 5 * 60 * 1000) {
|
|
return cached.tier || 'free';
|
|
}
|
|
}
|
|
|
|
if (!supabase) return 'free';
|
|
|
|
try {
|
|
const { data } = await supabase
|
|
.from('federation_servers')
|
|
.select('verified, tier')
|
|
.eq('guild_id', guildId)
|
|
.maybeSingle();
|
|
|
|
const tier = data?.tier || 'free';
|
|
federationCache.set(guildId, { verified: data?.verified, tier, timestamp: Date.now() });
|
|
return tier;
|
|
} catch (e) {
|
|
return 'free';
|
|
}
|
|
}
|
|
client.getServerTier = getServerTier;
|
|
|
|
client.on('error', (error) => {
|
|
console.error('[Discord] Client error:', error.message);
|
|
addErrorLog('discord', 'Discord client error', { error: error.message });
|
|
});
|
|
|
|
client.on('warn', (warning) => {
|
|
console.warn('[Discord] Warning:', warning);
|
|
addErrorLog('warning', 'Discord warning', { warning });
|
|
});
|
|
|
|
client.on('guildCreate', async (guild) => {
|
|
console.log(`[Guild] Joined new server: ${guild.name} (${guild.id}) - ${guild.memberCount} members`);
|
|
|
|
// Create default server config
|
|
if (supabase) {
|
|
try {
|
|
await supabase.from('server_config').upsert({
|
|
guild_id: guild.id,
|
|
guild_name: guild.name,
|
|
created_at: new Date().toISOString()
|
|
}, { onConflict: 'guild_id' });
|
|
} catch (e) {
|
|
console.warn('[Guild] Could not create server config:', e.message);
|
|
}
|
|
}
|
|
|
|
// Send welcome message to server owner or first available channel
|
|
const welcomeEmbed = {
|
|
color: 0x4A90E2,
|
|
title: '👋 Thanks for adding AeThex Bot!',
|
|
description: `Hello **${guild.name}**! I'm now ready to help your community grow with XP, leveling, music, and moderation features.`,
|
|
fields: [
|
|
{
|
|
name: '🎮 Core Features (Available Now)',
|
|
value: '• XP & Leveling System\n• Music Player\n• Basic Moderation\n• Achievements & Quests\n• Welcome/Goodbye Messages',
|
|
inline: true
|
|
},
|
|
{
|
|
name: '🛡️ Federation Features',
|
|
value: '• Cross-server ban sync\n• Reputation network\n• Sentinel protection\n• Premium slots\n\n*Requires verification*',
|
|
inline: true
|
|
},
|
|
{
|
|
name: '🚀 Getting Started',
|
|
value: '1. Use `/help` to see all commands\n2. Use `/config` to set up your server\n3. Use `/federation apply` to join the protection network'
|
|
},
|
|
{
|
|
name: '📖 Need Help?',
|
|
value: '[Documentation](https://bot.aethex.dev/commands) • [Support Server](https://discord.gg/aethex)'
|
|
}
|
|
],
|
|
footer: { text: 'AeThex Bot • Powering Communities' },
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
try {
|
|
const owner = await guild.fetchOwner();
|
|
await owner.send({ embeds: [welcomeEmbed] });
|
|
} catch (e) {
|
|
// Try to find a system channel or first text channel
|
|
const channel = guild.systemChannel || guild.channels.cache.find(
|
|
c => c.type === 0 && c.permissionsFor(guild.members.me)?.has('SendMessages')
|
|
);
|
|
if (channel) {
|
|
try {
|
|
await channel.send({ embeds: [welcomeEmbed] });
|
|
} catch (err) {}
|
|
}
|
|
}
|
|
});
|
|
|
|
// =============================================================================
|
|
// SENTINEL: TICKET TRACKING (New)
|
|
// =============================================================================
|
|
|
|
const activeTickets = new Map();
|
|
client.activeTickets = activeTickets;
|
|
|
|
async function loadActiveTickets() {
|
|
if (!supabase) return;
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('tickets')
|
|
.select('*')
|
|
.eq('status', 'open');
|
|
if (error) throw error;
|
|
for (const ticket of data || []) {
|
|
activeTickets.set(ticket.channel_id, {
|
|
odId: ticket.id,
|
|
userId: ticket.user_id,
|
|
guildId: ticket.guild_id,
|
|
reason: ticket.reason,
|
|
createdAt: new Date(ticket.created_at).getTime(),
|
|
});
|
|
}
|
|
console.log(`[Tickets] Loaded ${activeTickets.size} active tickets from database`);
|
|
} catch (e) {
|
|
console.warn('[Tickets] Could not load tickets:', e.message);
|
|
}
|
|
}
|
|
|
|
async function saveTicket(channelId, data) {
|
|
if (!supabase) return;
|
|
try {
|
|
await supabase.from('tickets').insert({
|
|
channel_id: channelId,
|
|
user_id: data.userId,
|
|
guild_id: data.guildId,
|
|
reason: data.reason,
|
|
status: 'open',
|
|
});
|
|
} catch (e) {
|
|
console.warn('[Tickets] Could not save ticket:', e.message);
|
|
}
|
|
}
|
|
|
|
async function closeTicket(channelId) {
|
|
if (!supabase) return;
|
|
try {
|
|
await supabase
|
|
.from('tickets')
|
|
.update({ status: 'closed', closed_at: new Date().toISOString() })
|
|
.eq('channel_id', channelId);
|
|
} catch (e) {
|
|
console.warn('[Tickets] Could not close ticket:', e.message);
|
|
}
|
|
}
|
|
|
|
client.saveTicket = saveTicket;
|
|
client.closeTicket = closeTicket;
|
|
|
|
// =============================================================================
|
|
// SENTINEL: ALERT SYSTEM (New)
|
|
// =============================================================================
|
|
|
|
let alertChannelId = process.env.ALERT_CHANNEL_ID;
|
|
client.alertChannelId = alertChannelId;
|
|
|
|
async function sendAlert(message, embed = null) {
|
|
if (!alertChannelId) return;
|
|
try {
|
|
const channel = await client.channels.fetch(alertChannelId);
|
|
if (channel) {
|
|
if (embed) {
|
|
await channel.send({ content: message, embeds: [embed] });
|
|
} else {
|
|
await channel.send(message);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to send alert:", err.message);
|
|
}
|
|
}
|
|
client.sendAlert = sendAlert;
|
|
|
|
// =============================================================================
|
|
// ACTIVITY FEED SYSTEM (New - Dashboard Real-time)
|
|
// =============================================================================
|
|
|
|
const activityFeed = [];
|
|
const MAX_ACTIVITY_EVENTS = 100;
|
|
const threatAlerts = [];
|
|
const MAX_THREAT_ALERTS = 50;
|
|
|
|
const errorLogs = [];
|
|
const MAX_ERROR_LOGS = 50;
|
|
const commandQueue = [];
|
|
const MAX_COMMAND_QUEUE = 100;
|
|
let lastCpuUsage = process.cpuUsage();
|
|
let lastCpuTime = Date.now();
|
|
|
|
function addErrorLog(type, message, details = {}) {
|
|
const log = {
|
|
id: Date.now() + Math.random().toString(36).substr(2, 9),
|
|
type,
|
|
message,
|
|
details,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
errorLogs.unshift(log);
|
|
if (errorLogs.length > MAX_ERROR_LOGS) {
|
|
errorLogs.pop();
|
|
}
|
|
return log;
|
|
}
|
|
|
|
function addToCommandQueue(command, status = 'pending') {
|
|
const entry = {
|
|
id: Date.now() + Math.random().toString(36).substr(2, 9),
|
|
command,
|
|
status,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
commandQueue.unshift(entry);
|
|
if (commandQueue.length > MAX_COMMAND_QUEUE) {
|
|
commandQueue.pop();
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
function updateCommandQueue(id, status) {
|
|
const entry = commandQueue.find(e => e.id === id);
|
|
if (entry) {
|
|
entry.status = status;
|
|
entry.completedAt = new Date().toISOString();
|
|
}
|
|
}
|
|
|
|
let currentCpuUsage = 0;
|
|
|
|
function updateCpuUsage() {
|
|
const now = Date.now();
|
|
const cpuUsage = process.cpuUsage(lastCpuUsage);
|
|
const elapsed = (now - lastCpuTime) * 1000;
|
|
|
|
if (elapsed > 0) {
|
|
const userPercent = (cpuUsage.user / elapsed) * 100;
|
|
const systemPercent = (cpuUsage.system / elapsed) * 100;
|
|
currentCpuUsage = Math.min(100, Math.round(userPercent + systemPercent));
|
|
}
|
|
|
|
lastCpuUsage = process.cpuUsage();
|
|
lastCpuTime = now;
|
|
}
|
|
|
|
setInterval(updateCpuUsage, 5000);
|
|
|
|
function getCpuUsage() {
|
|
return currentCpuUsage;
|
|
}
|
|
|
|
client.addErrorLog = addErrorLog;
|
|
client.errorLogs = errorLogs;
|
|
client.commandQueue = commandQueue;
|
|
client.addToCommandQueue = addToCommandQueue;
|
|
client.updateCommandQueue = updateCommandQueue;
|
|
|
|
// Analytics tracking
|
|
const analyticsData = {
|
|
commandUsage: {},
|
|
xpDistributed: 0,
|
|
newMembers: 0,
|
|
modActions: { warnings: 0, kicks: 0, bans: 0, timeouts: 0 },
|
|
hourlyActivity: Array(24).fill(0),
|
|
dailyActivity: Array(7).fill(0),
|
|
lastReset: Date.now(),
|
|
};
|
|
|
|
function trackCommand(commandName) {
|
|
analyticsData.commandUsage[commandName] = (analyticsData.commandUsage[commandName] || 0) + 1;
|
|
const hour = new Date().getHours();
|
|
const day = new Date().getDay();
|
|
analyticsData.hourlyActivity[hour]++;
|
|
analyticsData.dailyActivity[day]++;
|
|
}
|
|
|
|
function trackXP(amount) {
|
|
analyticsData.xpDistributed += amount;
|
|
}
|
|
|
|
function trackNewMember() {
|
|
analyticsData.newMembers++;
|
|
}
|
|
|
|
function trackModAction(type) {
|
|
if (analyticsData.modActions[type] !== undefined) {
|
|
analyticsData.modActions[type]++;
|
|
}
|
|
}
|
|
|
|
function resetDailyAnalytics() {
|
|
const now = Date.now();
|
|
const lastReset = analyticsData.lastReset;
|
|
const dayMs = 24 * 60 * 60 * 1000;
|
|
|
|
if (now - lastReset > dayMs) {
|
|
analyticsData.commandUsage = {};
|
|
analyticsData.xpDistributed = 0;
|
|
analyticsData.newMembers = 0;
|
|
analyticsData.modActions = { warnings: 0, kicks: 0, bans: 0, timeouts: 0 };
|
|
analyticsData.lastReset = now;
|
|
}
|
|
}
|
|
|
|
client.trackCommand = trackCommand;
|
|
client.trackXP = trackXP;
|
|
client.trackNewMember = trackNewMember;
|
|
client.trackModAction = trackModAction;
|
|
client.analyticsData = analyticsData;
|
|
|
|
function addActivity(type, data) {
|
|
const event = {
|
|
id: Date.now() + Math.random().toString(36).substr(2, 9),
|
|
type,
|
|
data,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
activityFeed.unshift(event);
|
|
if (activityFeed.length > MAX_ACTIVITY_EVENTS) {
|
|
activityFeed.pop();
|
|
}
|
|
return event;
|
|
}
|
|
|
|
function addThreatAlert(level, message, details = {}) {
|
|
const alert = {
|
|
id: Date.now() + Math.random().toString(36).substr(2, 9),
|
|
level,
|
|
message,
|
|
details,
|
|
timestamp: new Date().toISOString(),
|
|
resolved: false,
|
|
};
|
|
threatAlerts.unshift(alert);
|
|
if (threatAlerts.length > MAX_THREAT_ALERTS) {
|
|
threatAlerts.pop();
|
|
}
|
|
return alert;
|
|
}
|
|
|
|
client.addActivity = addActivity;
|
|
client.activityFeed = activityFeed;
|
|
client.addThreatAlert = addThreatAlert;
|
|
client.threatAlerts = threatAlerts;
|
|
|
|
// =============================================================================
|
|
// COMMAND LOADING
|
|
// =============================================================================
|
|
|
|
client.commands = new Collection();
|
|
|
|
const commandsPath = path.join(__dirname, "commands");
|
|
if (fs.existsSync(commandsPath)) {
|
|
const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js"));
|
|
for (const file of commandFiles) {
|
|
const filePath = path.join(commandsPath, file);
|
|
const command = require(filePath);
|
|
if ("data" in command && "execute" in command) {
|
|
client.commands.set(command.data.name, command);
|
|
console.log(`Loaded command: ${command.data.name}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// EVENT LOADING
|
|
// =============================================================================
|
|
|
|
const eventsPath = path.join(__dirname, "events");
|
|
if (fs.existsSync(eventsPath)) {
|
|
const eventFiles = fs.readdirSync(eventsPath).filter((file) => file.endsWith(".js"));
|
|
for (const file of eventFiles) {
|
|
const filePath = path.join(eventsPath, file);
|
|
const event = require(filePath);
|
|
if ("name" in event && "execute" in event) {
|
|
client.on(event.name, (...args) => event.execute(...args, client, supabase));
|
|
console.log(`Loaded event: ${event.name}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// SENTINEL LISTENER LOADING (New)
|
|
// =============================================================================
|
|
|
|
const sentinelPath = path.join(__dirname, "listeners", "sentinel");
|
|
if (fs.existsSync(sentinelPath)) {
|
|
const sentinelFiles = fs.readdirSync(sentinelPath).filter((file) => file.endsWith(".js"));
|
|
for (const file of sentinelFiles) {
|
|
const filePath = path.join(sentinelPath, file);
|
|
const listener = require(filePath);
|
|
if ("name" in listener && "execute" in listener) {
|
|
client.on(listener.name, (...args) => listener.execute(...args, client));
|
|
console.log(`Loaded sentinel listener: ${listener.name}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// GENERAL LISTENER LOADING (Welcome, Goodbye, XP Tracker)
|
|
// =============================================================================
|
|
|
|
const listenersPath = path.join(__dirname, "listeners");
|
|
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js', 'voiceXp.js', 'starboard.js', 'federationProtection.js', 'streamChecker.js'];
|
|
for (const file of generalListenerFiles) {
|
|
const filePath = path.join(listenersPath, file);
|
|
if (fs.existsSync(filePath)) {
|
|
const listener = require(filePath);
|
|
if ("name" in listener && "execute" in listener) {
|
|
// For 'ready' event: no args are passed, so call execute(client, supabase) directly
|
|
// For other events (messageCreate, etc.): args are passed, then client/supabase
|
|
if (listener.name === 'ready') {
|
|
if (listener.once) {
|
|
client.once(listener.name, () => listener.execute(client, supabase));
|
|
} else {
|
|
client.on(listener.name, () => listener.execute(client, supabase));
|
|
}
|
|
} else {
|
|
client.on(listener.name, (...args) => listener.execute(...args, client, supabase));
|
|
}
|
|
console.log(`Loaded listener: ${file}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// SERVER CONFIGS MAP
|
|
// =============================================================================
|
|
|
|
const serverConfigs = new Map();
|
|
client.serverConfigs = serverConfigs;
|
|
|
|
async function loadServerConfigs() {
|
|
if (!supabase) return;
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('server_config')
|
|
.select('*');
|
|
if (error) throw error;
|
|
for (const config of data || []) {
|
|
serverConfigs.set(config.guild_id, config);
|
|
}
|
|
console.log(`[Config] Loaded ${serverConfigs.size} server configurations`);
|
|
} catch (e) {
|
|
console.warn('[Config] Could not load server configs:', e.message);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// FEED SYNC SETUP (Modified: Guard for missing Supabase)
|
|
// =============================================================================
|
|
|
|
let feedSyncModule = null;
|
|
let setupFeedListener = null;
|
|
let sendPostToDiscord = null;
|
|
let getFeedChannelId = () => null;
|
|
|
|
try {
|
|
feedSyncModule = require("./listeners/feedSync");
|
|
setupFeedListener = feedSyncModule.setupFeedListener;
|
|
sendPostToDiscord = feedSyncModule.sendPostToDiscord;
|
|
getFeedChannelId = feedSyncModule.getFeedChannelId;
|
|
} catch (e) {
|
|
console.log("Feed sync module not available");
|
|
}
|
|
|
|
// =============================================================================
|
|
// INTERACTION HANDLER (Modified: Added button handling for tickets)
|
|
// =============================================================================
|
|
|
|
client.on("interactionCreate", async (interaction) => {
|
|
console.log(`[Interaction] Received: type=${interaction.type}, commandName=${interaction.commandName || 'N/A'}, user=${interaction.user?.tag}, guild=${interaction.guildId}`);
|
|
|
|
if (interaction.isChatInputCommand()) {
|
|
console.log(`[Command] Processing: ${interaction.commandName}`);
|
|
const command = client.commands.get(interaction.commandName);
|
|
if (!command) {
|
|
console.warn(`No command matching ${interaction.commandName} was found.`);
|
|
return;
|
|
}
|
|
|
|
const queueEntry = addToCommandQueue(`/${interaction.commandName} by ${interaction.user.tag}`, 'pending');
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
console.log(`[Command] Executing: ${interaction.commandName}`);
|
|
await command.execute(interaction, supabase, client);
|
|
const executionTime = Date.now() - startTime;
|
|
console.log(`[Command] Completed: ${interaction.commandName} (${executionTime}ms)`);
|
|
|
|
updateCommandQueue(queueEntry.id, 'completed');
|
|
trackCommand(interaction.commandName);
|
|
resetDailyAnalytics();
|
|
|
|
// Track command usage for achievements
|
|
if (supabase && interaction.guildId) {
|
|
trackCommandForAchievements(interaction.user.id, interaction.guildId, interaction.member, supabase, client).catch(() => {});
|
|
}
|
|
|
|
const activityData = {
|
|
command: interaction.commandName,
|
|
user: interaction.user.tag,
|
|
userId: interaction.user.id,
|
|
guild: interaction.guild?.name || 'DM',
|
|
guildId: interaction.guildId,
|
|
executionTime,
|
|
};
|
|
|
|
addActivity('command', activityData);
|
|
|
|
// Log to database
|
|
logCommand({
|
|
commandName: interaction.commandName,
|
|
userId: interaction.user.id,
|
|
userTag: interaction.user.tag,
|
|
guildId: interaction.guildId,
|
|
guildName: interaction.guild?.name || 'DM',
|
|
channelId: interaction.channelId,
|
|
success: true,
|
|
executionTime,
|
|
});
|
|
|
|
// Broadcast via WebSocket
|
|
if (typeof wsBroadcast === 'function') {
|
|
wsBroadcast('command', activityData);
|
|
}
|
|
} catch (error) {
|
|
const executionTime = Date.now() - startTime;
|
|
console.error(`Error executing ${interaction.commandName}:`, error);
|
|
|
|
updateCommandQueue(queueEntry.id, 'failed');
|
|
addErrorLog('command', `Error in /${interaction.commandName}`, {
|
|
command: interaction.commandName,
|
|
user: interaction.user.tag,
|
|
userId: interaction.user.id,
|
|
guild: interaction.guild?.name || 'DM',
|
|
error: error.message,
|
|
});
|
|
|
|
// Log failed command to database
|
|
logCommand({
|
|
commandName: interaction.commandName,
|
|
userId: interaction.user.id,
|
|
userTag: interaction.user.tag,
|
|
guildId: interaction.guildId,
|
|
guildName: interaction.guild?.name || 'DM',
|
|
channelId: interaction.channelId,
|
|
success: false,
|
|
errorMessage: error.message,
|
|
executionTime,
|
|
});
|
|
|
|
try {
|
|
const errorEmbed = new EmbedBuilder()
|
|
.setColor(0xff0000)
|
|
.setTitle("Command Error")
|
|
.setDescription("There was an error while executing this command.")
|
|
.setFooter({ text: "Contact support if this persists" });
|
|
|
|
if (interaction.replied || interaction.deferred) {
|
|
await interaction.followUp({ embeds: [errorEmbed], ephemeral: true }).catch(() => {});
|
|
} else {
|
|
await interaction.reply({ embeds: [errorEmbed], ephemeral: true }).catch(() => {});
|
|
}
|
|
} catch (replyError) {
|
|
console.error("Failed to send error response:", replyError.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (interaction.isButton()) {
|
|
const [action, ...params] = interaction.customId.split('_');
|
|
|
|
if (action === 'ticket') {
|
|
const ticketAction = params[0];
|
|
if (ticketAction === 'close') {
|
|
try {
|
|
const channel = interaction.channel;
|
|
if (channel && channel.type === ChannelType.GuildText) {
|
|
await interaction.reply({ content: 'Closing ticket...', ephemeral: true });
|
|
activeTickets.delete(channel.id);
|
|
await closeTicket(channel.id);
|
|
setTimeout(() => channel.delete().catch(console.error), 3000);
|
|
}
|
|
} catch (err) {
|
|
console.error('Ticket close error:', err);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (action === 'role') {
|
|
const roleId = params.join('_');
|
|
try {
|
|
const role = interaction.guild.roles.cache.get(roleId);
|
|
if (!role) {
|
|
return interaction.reply({ content: 'This role no longer exists.', ephemeral: true });
|
|
}
|
|
|
|
const member = interaction.member;
|
|
if (member.roles.cache.has(roleId)) {
|
|
await member.roles.remove(roleId);
|
|
await interaction.reply({ content: `Removed ${role} from you!`, ephemeral: true });
|
|
} else {
|
|
await member.roles.add(roleId);
|
|
await interaction.reply({ content: `Added ${role} to you!`, ephemeral: true });
|
|
}
|
|
} catch (err) {
|
|
console.error('Role button error:', err);
|
|
await interaction.reply({ content: 'Failed to toggle role. Check bot permissions.', ephemeral: true }).catch(() => {});
|
|
}
|
|
}
|
|
|
|
if (action === 'giveaway') {
|
|
const giveawayAction = params[0];
|
|
if (giveawayAction === 'enter') {
|
|
try {
|
|
const messageId = interaction.message.id;
|
|
|
|
let giveawayData = client.giveaways?.get(messageId);
|
|
let entries = giveawayData?.entries || [];
|
|
|
|
if (supabase) {
|
|
const { data } = await supabase
|
|
.from('giveaways')
|
|
.select('*')
|
|
.eq('message_id', messageId)
|
|
.single();
|
|
|
|
if (data) {
|
|
entries = data.entries || [];
|
|
giveawayData = data;
|
|
}
|
|
}
|
|
|
|
if (!giveawayData) {
|
|
return interaction.reply({ content: 'This giveaway is no longer active.', ephemeral: true });
|
|
}
|
|
|
|
if (giveawayData.required_role) {
|
|
const hasRole = interaction.member.roles.cache.has(giveawayData.required_role);
|
|
if (!hasRole) {
|
|
return interaction.reply({ content: `You need the <@&${giveawayData.required_role}> role to enter!`, ephemeral: true });
|
|
}
|
|
}
|
|
|
|
if (entries.includes(interaction.user.id)) {
|
|
return interaction.reply({ content: 'You have already entered this giveaway!', ephemeral: true });
|
|
}
|
|
|
|
entries.push(interaction.user.id);
|
|
|
|
if (client.giveaways?.has(messageId)) {
|
|
client.giveaways.get(messageId).entries = entries;
|
|
}
|
|
|
|
if (supabase) {
|
|
await supabase
|
|
.from('giveaways')
|
|
.update({ entries: entries })
|
|
.eq('message_id', messageId);
|
|
}
|
|
|
|
const embed = EmbedBuilder.from(interaction.message.embeds[0]);
|
|
const entriesField = embed.data.fields?.find(f => f.name.includes('Entries'));
|
|
if (entriesField) {
|
|
entriesField.value = `${entries.length}`;
|
|
}
|
|
await interaction.message.edit({ embeds: [embed] });
|
|
|
|
await interaction.reply({ content: `You have entered the giveaway! Total entries: ${entries.length}`, ephemeral: true });
|
|
|
|
} catch (err) {
|
|
console.error('Giveaway entry error:', err);
|
|
await interaction.reply({ content: 'Failed to enter giveaway.', ephemeral: true }).catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (action === 'backup') {
|
|
const backupAction = params[0];
|
|
|
|
if (backupAction === 'restore' || backupAction === 'delete') {
|
|
const backupId = params[1];
|
|
|
|
if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
|
|
return interaction.reply({ content: 'Only administrators can manage backups.', ephemeral: true });
|
|
}
|
|
|
|
if (backupAction === 'delete') {
|
|
try {
|
|
const { data: backup } = await supabase
|
|
.from('server_backups')
|
|
.select('name')
|
|
.eq('id', backupId)
|
|
.eq('guild_id', interaction.guildId)
|
|
.single();
|
|
|
|
if (!backup) {
|
|
return interaction.reply({ content: 'Backup not found.', ephemeral: true });
|
|
}
|
|
|
|
await supabase.from('server_backups').delete().eq('id', backupId);
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(0x00ff00)
|
|
.setTitle('Backup Deleted')
|
|
.setDescription(`Successfully deleted backup: **${backup.name}**`)
|
|
.setTimestamp();
|
|
|
|
await interaction.update({ embeds: [embed], components: [] });
|
|
} catch (err) {
|
|
console.error('Backup delete button error:', err);
|
|
await interaction.reply({ content: 'Failed to delete backup.', ephemeral: true }).catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (backupAction === 'confirm') {
|
|
if (params[1] === 'restore') {
|
|
const backupId = params[2];
|
|
const components = params[3] || 'all';
|
|
|
|
try {
|
|
await interaction.deferUpdate();
|
|
|
|
const { data: backup } = await supabase
|
|
.from('server_backups')
|
|
.select('*')
|
|
.eq('id', backupId)
|
|
.eq('guild_id', interaction.guildId)
|
|
.single();
|
|
|
|
if (!backup) {
|
|
return interaction.editReply({ content: 'Backup not found.', embeds: [], components: [] });
|
|
}
|
|
|
|
const { createBackup, performRestore } = require('./commands/backup');
|
|
|
|
const preRestoreBackup = await createBackup(interaction.guild, supabase, interaction.guildId);
|
|
await supabase.from('server_backups').insert({
|
|
guild_id: interaction.guildId,
|
|
name: `Pre-restore backup (${new Date().toLocaleDateString()})`,
|
|
description: 'Automatic backup before restore',
|
|
backup_type: 'auto',
|
|
created_by: interaction.user.id,
|
|
data: preRestoreBackup,
|
|
roles_count: preRestoreBackup.roles?.length || 0,
|
|
channels_count: preRestoreBackup.channels?.length || 0,
|
|
size_bytes: JSON.stringify(preRestoreBackup).length
|
|
});
|
|
|
|
const results = await performRestore(interaction.guild, backup.data, components, supabase);
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(0x00ff00)
|
|
.setTitle('Restore Complete')
|
|
.setDescription(`Restored from backup: **${backup.name}**`)
|
|
.addFields(
|
|
{ name: 'Roles', value: `Created: ${results.roles.created} | Skipped: ${results.roles.skipped} | Failed: ${results.roles.failed}`, inline: false },
|
|
{ name: 'Channels', value: `Created: ${results.channels.created} | Skipped: ${results.channels.skipped} | Failed: ${results.channels.failed}`, inline: false },
|
|
{ name: 'Bot Config', value: results.botConfig ? 'Restored' : 'Not restored', inline: true }
|
|
)
|
|
.setFooter({ text: 'A backup was created before this restore' })
|
|
.setTimestamp();
|
|
|
|
await interaction.editReply({ embeds: [embed], components: [] });
|
|
} catch (err) {
|
|
console.error('Backup restore button error:', err);
|
|
await interaction.editReply({ content: 'Failed to restore backup.', embeds: [], components: [] }).catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (backupAction === 'cancel') {
|
|
await interaction.update({ content: 'Restore cancelled.', embeds: [], components: [] });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (interaction.isModalSubmit()) {
|
|
if (interaction.customId.startsWith('embed_modal_')) {
|
|
try {
|
|
const parts = interaction.customId.split('_');
|
|
const channelId = parts[2];
|
|
const color = parseInt(parts[3], 16);
|
|
|
|
const title = interaction.fields.getTextInputValue('embed_title');
|
|
const description = interaction.fields.getTextInputValue('embed_description');
|
|
const imageUrl = interaction.fields.getTextInputValue('embed_image') || null;
|
|
const thumbnailUrl = interaction.fields.getTextInputValue('embed_thumbnail') || null;
|
|
const footerText = interaction.fields.getTextInputValue('embed_footer') || null;
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(color)
|
|
.setTitle(title)
|
|
.setDescription(description)
|
|
.setTimestamp();
|
|
|
|
if (imageUrl) embed.setImage(imageUrl);
|
|
if (thumbnailUrl) embed.setThumbnail(thumbnailUrl);
|
|
if (footerText) embed.setFooter({ text: footerText });
|
|
|
|
const channel = await client.channels.fetch(channelId);
|
|
await channel.send({ embeds: [embed] });
|
|
|
|
await interaction.reply({ content: `Embed sent to <#${channelId}>!`, ephemeral: true });
|
|
|
|
} catch (err) {
|
|
console.error('Embed modal error:', err);
|
|
await interaction.reply({ content: 'Failed to send embed. Check permissions and URLs.', ephemeral: true }).catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (interaction.isStringSelectMenu()) {
|
|
if (interaction.customId === 'help_category') {
|
|
try {
|
|
const category = interaction.values[0];
|
|
const { getCategoryEmbed } = require('./commands/help');
|
|
const embed = getCategoryEmbed(category);
|
|
await interaction.update({ embeds: [embed] });
|
|
} catch (err) {
|
|
console.error('Help select error:', err);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// =============================================================================
|
|
// COMMANDS FOR REGISTRATION (Uses actual command file definitions)
|
|
// =============================================================================
|
|
|
|
function getCommandsToRegister() {
|
|
return Array.from(client.commands.values()).map(cmd => cmd.data.toJSON());
|
|
}
|
|
|
|
// =============================================================================
|
|
// COMMAND REGISTRATION FUNCTION
|
|
// =============================================================================
|
|
|
|
async function registerDiscordCommands() {
|
|
try {
|
|
const rest = new REST({ version: "10" }).setToken(
|
|
process.env.DISCORD_BOT_TOKEN,
|
|
);
|
|
|
|
const commandsToRegister = getCommandsToRegister();
|
|
console.log(
|
|
`Registering ${commandsToRegister.length} slash commands...`,
|
|
);
|
|
|
|
try {
|
|
const data = await rest.put(
|
|
Routes.applicationCommands(process.env.DISCORD_CLIENT_ID),
|
|
{ body: commandsToRegister },
|
|
);
|
|
|
|
console.log(`Successfully registered ${data.length} slash commands`);
|
|
return { success: true, count: data.length, results: null };
|
|
} catch (bulkError) {
|
|
if (bulkError.code === 50240) {
|
|
console.warn(
|
|
"Error 50240: Entry Point detected. Registering individually...",
|
|
);
|
|
|
|
const results = [];
|
|
let successCount = 0;
|
|
let skipCount = 0;
|
|
|
|
for (const command of commandsToRegister) {
|
|
try {
|
|
const posted = await rest.post(
|
|
Routes.applicationCommands(process.env.DISCORD_CLIENT_ID),
|
|
{ body: command },
|
|
);
|
|
results.push({
|
|
name: command.name,
|
|
status: "registered",
|
|
id: posted.id,
|
|
});
|
|
successCount++;
|
|
} catch (postError) {
|
|
if (postError.code === 50045) {
|
|
results.push({
|
|
name: command.name,
|
|
status: "already_exists",
|
|
});
|
|
skipCount++;
|
|
} else {
|
|
results.push({
|
|
name: command.name,
|
|
status: "error",
|
|
error: postError.message,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`Registration complete: ${successCount} new, ${skipCount} already existed`,
|
|
);
|
|
return {
|
|
success: true,
|
|
count: successCount,
|
|
skipped: skipCount,
|
|
results,
|
|
};
|
|
}
|
|
|
|
throw bulkError;
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to register commands:", error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// HTTP SERVER (Modified: Added Sentinel stats to health endpoint)
|
|
// =============================================================================
|
|
|
|
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 /servers - Get official AeThex servers and all connected servers
|
|
if (req.url === "/whitelist" && req.method === "GET") {
|
|
const officialServers = AETHEX_OFFICIAL_GUILDS.map(guildId => {
|
|
const guild = client.guilds.cache.get(guildId);
|
|
return {
|
|
id: guildId,
|
|
name: guild?.name || 'Not Connected',
|
|
memberCount: guild?.memberCount || 0,
|
|
connected: !!guild,
|
|
official: true,
|
|
};
|
|
});
|
|
|
|
const allServers = client.guilds.cache.map(guild => ({
|
|
id: guild.id,
|
|
name: guild.name,
|
|
memberCount: guild.memberCount,
|
|
connected: true,
|
|
official: AETHEX_OFFICIAL_GUILDS.includes(guild.id),
|
|
}));
|
|
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
officialServers,
|
|
allServers,
|
|
users: whitelistedUsers.map(id => ({ id, note: 'Whitelisted User' })),
|
|
timestamp: new Date().toISOString(),
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// GET /roles - Get level roles and federation roles
|
|
if (req.url === "/roles" && req.method === "GET") {
|
|
(async () => {
|
|
const levelRoles = [];
|
|
const fedRoles = Array.from(federationMappings.entries()).map(([roleId, data]) => ({
|
|
roleId,
|
|
name: data.name,
|
|
guildId: data.guildId,
|
|
guildName: data.guildName,
|
|
linkedAt: new Date(data.linkedAt).toISOString(),
|
|
}));
|
|
|
|
// Try to get level roles from Supabase
|
|
if (supabase) {
|
|
try {
|
|
const { data } = await supabase.from('level_roles').select('*');
|
|
if (data) {
|
|
for (const lr of data) {
|
|
const guild = client.guilds.cache.get(lr.guild_id);
|
|
const role = guild?.roles.cache.get(lr.role_id);
|
|
levelRoles.push({
|
|
guildId: lr.guild_id,
|
|
guildName: guild?.name || 'Unknown',
|
|
roleId: lr.role_id,
|
|
roleName: role?.name || 'Unknown',
|
|
levelRequired: lr.level_required,
|
|
});
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Could not fetch level roles:', e.message);
|
|
}
|
|
}
|
|
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
levelRoles,
|
|
federationRoles: fedRoles,
|
|
timestamp: new Date().toISOString(),
|
|
}));
|
|
})();
|
|
return;
|
|
}
|
|
|
|
// POST /announce - Send announcement to servers
|
|
if (req.url === "/announce" && req.method === "POST") {
|
|
if (!checkAdminAuth(req)) {
|
|
res.writeHead(401);
|
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
return;
|
|
}
|
|
|
|
let body = "";
|
|
req.on("data", chunk => body += chunk);
|
|
req.on("end", async () => {
|
|
try {
|
|
const { title, message, targets, color } = JSON.parse(body);
|
|
|
|
if (!title || !message) {
|
|
res.writeHead(400);
|
|
res.end(JSON.stringify({ error: "Title and message required" }));
|
|
return;
|
|
}
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setTitle(title)
|
|
.setDescription(message)
|
|
.setColor(color ? parseInt(color.replace('#', ''), 16) : 0x5865F2)
|
|
.setTimestamp()
|
|
.setFooter({ text: 'AeThex Network Announcement' });
|
|
|
|
const results = [];
|
|
const targetGuilds = targets && targets.length > 0
|
|
? targets
|
|
: Array.from(client.guilds.cache.keys());
|
|
|
|
for (const guildId of targetGuilds) {
|
|
const guild = client.guilds.cache.get(guildId);
|
|
if (!guild) {
|
|
results.push({ guildId, success: false, error: 'Guild not found' });
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const config = serverConfigs.get(guildId);
|
|
let channel = null;
|
|
|
|
if (config?.welcome_channel) {
|
|
channel = guild.channels.cache.get(config.welcome_channel);
|
|
}
|
|
|
|
if (!channel) {
|
|
channel = guild.systemChannel ||
|
|
guild.channels.cache.find(c => c.type === 0 && c.permissionsFor(guild.members.me).has('SendMessages'));
|
|
}
|
|
|
|
if (channel) {
|
|
await channel.send({ embeds: [embed] });
|
|
results.push({ guildId, guildName: guild.name, success: true });
|
|
} else {
|
|
results.push({ guildId, guildName: guild.name, success: false, error: 'No suitable channel' });
|
|
}
|
|
} catch (error) {
|
|
results.push({ guildId, guildName: guild?.name, success: false, error: error.message });
|
|
}
|
|
}
|
|
|
|
addActivity('announcement', { title, targetCount: targetGuilds.length, successCount: results.filter(r => r.success).length });
|
|
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ success: true, results }));
|
|
} catch (error) {
|
|
res.writeHead(500);
|
|
res.end(JSON.stringify({ error: error.message }));
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// GET /servers - List all connected servers (for dropdowns)
|
|
if (req.url === "/servers" && req.method === "GET") {
|
|
const servers = client.guilds.cache.map(g => ({
|
|
id: g.id,
|
|
name: g.name,
|
|
icon: g.iconURL({ size: 64 }),
|
|
memberCount: g.memberCount,
|
|
}));
|
|
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ servers }));
|
|
return;
|
|
}
|
|
|
|
// GET /user-lookup/:query - Search for a user by ID or username
|
|
if (req.url.startsWith("/user-lookup/") && req.method === "GET") {
|
|
const query = decodeURIComponent(req.url.split("/user-lookup/")[1].split("?")[0]);
|
|
|
|
if (!query || query.length < 2) {
|
|
res.writeHead(400);
|
|
res.end(JSON.stringify({ error: "Query too short" }));
|
|
return;
|
|
}
|
|
|
|
(async () => {
|
|
try {
|
|
const results = [];
|
|
const isNumericId = /^\d{17,19}$/.test(query);
|
|
|
|
for (const guild of client.guilds.cache.values()) {
|
|
try {
|
|
if (isNumericId) {
|
|
const member = await guild.members.fetch(query).catch(() => null);
|
|
if (member) {
|
|
const existingIdx = results.findIndex(r => r.id === member.id);
|
|
if (existingIdx === -1) {
|
|
results.push({
|
|
id: member.id,
|
|
tag: member.user.tag,
|
|
username: member.user.username,
|
|
displayName: member.displayName,
|
|
avatar: member.user.displayAvatarURL({ size: 128 }),
|
|
bot: member.user.bot,
|
|
createdAt: member.user.createdAt.toISOString(),
|
|
joinedAt: member.joinedAt?.toISOString(),
|
|
roles: member.roles.cache.filter(r => r.name !== '@everyone').map(r => ({ id: r.id, name: r.name, color: r.hexColor })).slice(0, 10),
|
|
servers: [{ id: guild.id, name: guild.name }],
|
|
heat: getHeat(member.id),
|
|
});
|
|
} else {
|
|
results[existingIdx].servers.push({ id: guild.id, name: guild.name });
|
|
}
|
|
}
|
|
} else {
|
|
const members = await guild.members.fetch({ query, limit: 10 }).catch(() => new Map());
|
|
for (const member of members.values()) {
|
|
const existingIdx = results.findIndex(r => r.id === member.id);
|
|
if (existingIdx === -1) {
|
|
results.push({
|
|
id: member.id,
|
|
tag: member.user.tag,
|
|
username: member.user.username,
|
|
displayName: member.displayName,
|
|
avatar: member.user.displayAvatarURL({ size: 128 }),
|
|
bot: member.user.bot,
|
|
createdAt: member.user.createdAt.toISOString(),
|
|
joinedAt: member.joinedAt?.toISOString(),
|
|
roles: member.roles.cache.filter(r => r.name !== '@everyone').map(r => ({ id: r.id, name: r.name, color: r.hexColor })).slice(0, 10),
|
|
servers: [{ id: guild.id, name: guild.name }],
|
|
heat: getHeat(member.id),
|
|
});
|
|
} else {
|
|
results[existingIdx].servers.push({ id: guild.id, name: guild.name });
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
// Fetch Supabase profile data if available
|
|
if (supabase && results.length > 0) {
|
|
for (const user of results) {
|
|
try {
|
|
const { data: link } = await supabase
|
|
.from('discord_links')
|
|
.select('user_id, primary_arm')
|
|
.eq('discord_id', user.id)
|
|
.single();
|
|
|
|
if (link) {
|
|
user.linked = true;
|
|
user.realm = link.primary_arm;
|
|
|
|
const { data: profile } = await supabase
|
|
.from('user_profiles')
|
|
.select('username, xp, daily_streak, badges')
|
|
.eq('id', link.user_id)
|
|
.single();
|
|
|
|
if (profile) {
|
|
user.aethexUsername = profile.username;
|
|
user.xp = profile.xp || 0;
|
|
user.level = Math.floor(Math.sqrt((profile.xp || 0) / 100));
|
|
user.dailyStreak = profile.daily_streak || 0;
|
|
user.badges = profile.badges || [];
|
|
}
|
|
}
|
|
|
|
const { data: warnings } = await supabase
|
|
.from('warnings')
|
|
.select('id, reason, created_at')
|
|
.eq('user_id', user.id)
|
|
.order('created_at', { ascending: false })
|
|
.limit(5);
|
|
|
|
user.warnings = warnings || [];
|
|
|
|
const { data: modActions } = await supabase
|
|
.from('mod_actions')
|
|
.select('action, reason, created_at')
|
|
.eq('user_id', user.id)
|
|
.order('created_at', { ascending: false })
|
|
.limit(5);
|
|
|
|
user.modHistory = modActions || [];
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
results: results.slice(0, 20),
|
|
count: results.length,
|
|
query,
|
|
timestamp: new Date().toISOString(),
|
|
}));
|
|
} catch (error) {
|
|
res.writeHead(500);
|
|
res.end(JSON.stringify({ error: error.message }));
|
|
}
|
|
})();
|
|
return;
|
|
}
|
|
|
|
// GET /mod-actions - Get recent moderation actions
|
|
if (req.url === "/mod-actions" && req.method === "GET") {
|
|
if (!supabase) {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ success: false, message: "Supabase not configured", actions: [] }));
|
|
return;
|
|
}
|
|
|
|
(async () => {
|
|
try {
|
|
const { data: actions, error } = await supabase
|
|
.from('mod_actions')
|
|
.select('*')
|
|
.order('created_at', { ascending: false })
|
|
.limit(50);
|
|
|
|
if (error) throw error;
|
|
|
|
const { count: warnCount } = await supabase.from('warnings').select('*', { count: 'exact', head: true });
|
|
const { count: banCount } = await supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('action', 'ban');
|
|
const { count: kickCount } = await supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('action', 'kick');
|
|
const { count: timeoutCount } = await supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('action', 'timeout');
|
|
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
actions: actions || [],
|
|
counts: {
|
|
warnings: warnCount || 0,
|
|
bans: banCount || 0,
|
|
kicks: kickCount || 0,
|
|
timeouts: timeoutCount || 0,
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
}));
|
|
} catch (error) {
|
|
res.writeHead(500);
|
|
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
}
|
|
})();
|
|
return;
|
|
}
|
|
|
|
// GET /studio-feed - Get AeThex Studio community feed
|
|
if (req.url === "/studio-feed" && req.method === "GET") {
|
|
if (!supabase) {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
success: false,
|
|
message: "Supabase not configured - Studio feed unavailable",
|
|
posts: []
|
|
}));
|
|
return;
|
|
}
|
|
|
|
(async () => {
|
|
try {
|
|
const { data: posts, error } = await supabase
|
|
.from('community_posts')
|
|
.select('*')
|
|
.order('created_at', { ascending: false })
|
|
.limit(20);
|
|
|
|
if (error) throw error;
|
|
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
posts: posts || [],
|
|
count: posts?.length || 0,
|
|
timestamp: new Date().toISOString(),
|
|
}));
|
|
} catch (error) {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
success: false,
|
|
message: "No community posts table available",
|
|
posts: []
|
|
}));
|
|
}
|
|
})();
|
|
return;
|
|
}
|
|
|
|
// GET /foundation-stats - Get AeThex Foundation statistics
|
|
if (req.url === "/foundation-stats" && req.method === "GET") {
|
|
(async () => {
|
|
const foundationGuild = client.guilds.cache.get(REALM_GUILDS.foundation);
|
|
|
|
const stats = {
|
|
success: true,
|
|
contributors: 0,
|
|
projects: 0,
|
|
commits: 0,
|
|
members: foundationGuild?.memberCount || 0,
|
|
activity: [],
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
if (supabase) {
|
|
try {
|
|
const { count: contributorCount } = await supabase
|
|
.from('user_profiles')
|
|
.select('*', { count: 'exact', head: true })
|
|
.not('foundation_contributions', 'is', null);
|
|
stats.contributors = contributorCount || 0;
|
|
|
|
const { data: activity } = await supabase
|
|
.from('foundation_activity')
|
|
.select('*')
|
|
.order('created_at', { ascending: false })
|
|
.limit(10);
|
|
|
|
if (activity) {
|
|
stats.activity = activity.map(a => ({
|
|
type: a.type || 'commit',
|
|
message: a.message || a.description,
|
|
author: a.author || a.username,
|
|
date: a.created_at,
|
|
}));
|
|
}
|
|
} catch (e) {
|
|
// Tables may not exist, use defaults
|
|
}
|
|
}
|
|
|
|
// Estimate projects from federation mappings
|
|
stats.projects = federationMappings.size || 5;
|
|
stats.commits = Math.floor(Math.random() * 50) + 100; // Placeholder
|
|
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify(stats));
|
|
})();
|
|
return;
|
|
}
|
|
|
|
// POST /test-webhook - Test a Discord webhook
|
|
if (req.url === "/test-webhook" && req.method === "POST") {
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', async () => {
|
|
try {
|
|
const { url, message, username } = JSON.parse(body);
|
|
|
|
if (!url || !url.includes('discord.com/api/webhooks')) {
|
|
res.writeHead(400);
|
|
res.end(JSON.stringify({ success: false, error: 'Invalid webhook URL' }));
|
|
return;
|
|
}
|
|
|
|
if (!message) {
|
|
res.writeHead(400);
|
|
res.end(JSON.stringify({ success: false, error: 'Message is required' }));
|
|
return;
|
|
}
|
|
|
|
const webhookPayload = {
|
|
content: message,
|
|
username: username || 'AeThex Dashboard Test',
|
|
};
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(webhookPayload),
|
|
});
|
|
|
|
if (response.ok || response.status === 204) {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ success: true, message: 'Webhook test sent successfully' }));
|
|
} else {
|
|
const errorText = await response.text();
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ success: false, error: `Webhook failed: ${response.status} - ${errorText}` }));
|
|
}
|
|
} catch (error) {
|
|
res.writeHead(500);
|
|
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Analytics endpoint with detailed command data
|
|
if (req.url === "/command-analytics" || req.url.startsWith("/command-analytics?")) {
|
|
(async () => {
|
|
try {
|
|
const url = new URL(req.url, `http://localhost:${healthPort}`);
|
|
const days = parseInt(url.searchParams.get('days') || '7');
|
|
const analytics = await getCommandAnalytics(days);
|
|
const totalCount = await getTotalCommandCount();
|
|
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
totalCommands: totalCount,
|
|
analytics,
|
|
timestamp: new Date().toISOString(),
|
|
}));
|
|
} catch (error) {
|
|
res.writeHead(500);
|
|
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
}
|
|
})();
|
|
return;
|
|
}
|
|
|
|
res.writeHead(404);
|
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
});
|
|
|
|
// =============================================================================
|
|
// WEBSOCKET SERVER FOR REAL-TIME UPDATES
|
|
// =============================================================================
|
|
|
|
const wsClients = new Set();
|
|
const wss = new WebSocket.Server({ noServer: true });
|
|
|
|
wss.on('connection', (ws) => {
|
|
wsClients.add(ws);
|
|
console.log(`[WebSocket] Client connected. Total: ${wsClients.size}`);
|
|
|
|
ws.send(JSON.stringify({
|
|
type: 'init',
|
|
data: {
|
|
status: 'online',
|
|
guilds: client.guilds.cache.size,
|
|
commands: client.commands?.size || 0,
|
|
uptime: Math.floor(process.uptime()),
|
|
heatMapSize: heatMap.size,
|
|
activeTickets: activeTickets.size,
|
|
federationLinks: federationMappings.size,
|
|
}
|
|
}));
|
|
|
|
ws.on('close', () => {
|
|
wsClients.delete(ws);
|
|
console.log(`[WebSocket] Client disconnected. Total: ${wsClients.size}`);
|
|
});
|
|
|
|
ws.on('error', (err) => {
|
|
console.error('[WebSocket] Error:', err.message);
|
|
wsClients.delete(ws);
|
|
});
|
|
});
|
|
|
|
function wsBroadcast(type, data) {
|
|
const message = JSON.stringify({ type, data, timestamp: new Date().toISOString() });
|
|
for (const wsClient of wsClients) {
|
|
if (wsClient.readyState === WebSocket.OPEN) {
|
|
try {
|
|
wsClient.send(message);
|
|
} catch (err) {
|
|
console.error('[WebSocket] Broadcast error:', err.message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
setInterval(() => {
|
|
if (wsClients.size > 0) {
|
|
wsBroadcast('stats', {
|
|
guilds: client.guilds.cache.size,
|
|
commands: client.commands?.size || 0,
|
|
uptime: Math.floor(process.uptime()),
|
|
heatMapSize: heatMap.size,
|
|
activeTickets: activeTickets.size,
|
|
federationLinks: federationMappings.size,
|
|
memory: process.memoryUsage(),
|
|
cpu: getCpuUsage(),
|
|
});
|
|
}
|
|
}, 5000);
|
|
|
|
// Add WebSocket upgrade handling
|
|
httpServer.on('upgrade', (request, socket, head) => {
|
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
wss.emit('connection', ws, request);
|
|
});
|
|
});
|
|
|
|
httpServer.listen(healthPort, () => {
|
|
console.log(`Health/API server running on port ${healthPort}`);
|
|
console.log(`WebSocket server available at ws://localhost:${healthPort}`);
|
|
console.log(`Register commands at: POST http://localhost:${healthPort}/register-commands`);
|
|
});
|
|
|
|
// =============================================================================
|
|
// EXPRESS WEB PORTAL (Discord OAuth, User Dashboard, API)
|
|
// =============================================================================
|
|
|
|
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);
|
|
startFederationTrustEvaluator(client, supabase);
|
|
}
|
|
});
|
|
|
|
// =============================================================================
|
|
// AUTOMATIC BACKUP SCHEDULER
|
|
// =============================================================================
|
|
|
|
async function startAutoBackupScheduler(discordClient, supabaseClient) {
|
|
console.log('[Backup] Starting automatic backup scheduler...');
|
|
|
|
// Check every hour for servers that need backups
|
|
setInterval(async () => {
|
|
try {
|
|
const { data: settings } = await supabaseClient
|
|
.from('backup_settings')
|
|
.select('*')
|
|
.eq('auto_enabled', true);
|
|
|
|
if (!settings || settings.length === 0) return;
|
|
|
|
for (const setting of settings) {
|
|
const guild = discordClient.guilds.cache.get(setting.guild_id);
|
|
if (!guild) continue;
|
|
|
|
// Check if backup is needed
|
|
const { data: lastBackup } = await supabaseClient
|
|
.from('server_backups')
|
|
.select('created_at')
|
|
.eq('guild_id', setting.guild_id)
|
|
.eq('backup_type', 'auto')
|
|
.order('created_at', { ascending: false })
|
|
.limit(1)
|
|
.single();
|
|
|
|
const intervalMs = (setting.interval_hours || 24) * 60 * 60 * 1000;
|
|
const lastBackupTime = lastBackup ? new Date(lastBackup.created_at).getTime() : 0;
|
|
const now = Date.now();
|
|
|
|
if (now - lastBackupTime >= intervalMs) {
|
|
console.log(`[Backup] Creating auto backup for guild ${guild.name}`);
|
|
|
|
const { createBackup } = require('./commands/backup');
|
|
const backupData = await createBackup(guild, supabaseClient, setting.guild_id);
|
|
|
|
await supabaseClient.from('server_backups').insert({
|
|
guild_id: setting.guild_id,
|
|
name: `Auto Backup - ${new Date().toLocaleDateString()}`,
|
|
description: 'Scheduled automatic backup',
|
|
backup_type: 'auto',
|
|
created_by: null,
|
|
data: backupData,
|
|
roles_count: backupData.roles?.length || 0,
|
|
channels_count: backupData.channels?.length || 0,
|
|
size_bytes: JSON.stringify(backupData).length
|
|
});
|
|
|
|
// Clean up old backups
|
|
const maxBackups = setting.max_backups || 7;
|
|
const { data: allBackups } = await supabaseClient
|
|
.from('server_backups')
|
|
.select('id')
|
|
.eq('guild_id', setting.guild_id)
|
|
.eq('backup_type', 'auto')
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (allBackups && allBackups.length > maxBackups) {
|
|
const toDelete = allBackups.slice(maxBackups).map(b => b.id);
|
|
await supabaseClient.from('server_backups').delete().in('id', toDelete);
|
|
console.log(`[Backup] Cleaned up ${toDelete.length} old backups for ${guild.name}`);
|
|
}
|
|
|
|
console.log(`[Backup] Auto backup complete for ${guild.name}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('[Backup] Auto backup error:', error.message);
|
|
}
|
|
}, 60 * 60 * 1000); // Check every hour
|
|
|
|
console.log('[Backup] Scheduler started - checking every hour');
|
|
}
|
|
|
|
// =============================================================================
|
|
// FEDERATION TRUST LEVEL PROGRESSION SCHEDULER
|
|
// =============================================================================
|
|
|
|
async function startFederationTrustEvaluator(discordClient, supabaseClient) {
|
|
console.log('[Federation] Starting trust level evaluation scheduler...');
|
|
|
|
const { calculateTrustLevel, getTrustLevelInfo } = require('./utils/trustLevels');
|
|
|
|
// Evaluate trust levels daily
|
|
setInterval(async () => {
|
|
try {
|
|
const { data: servers } = await supabaseClient
|
|
.from('federation_servers')
|
|
.select('*')
|
|
.eq('status', 'approved');
|
|
|
|
if (!servers || servers.length === 0) return;
|
|
|
|
let upgrades = 0;
|
|
let downgrades = 0;
|
|
|
|
for (const server of servers) {
|
|
// Get current member count from Discord
|
|
const guild = discordClient.guilds.cache.get(server.guild_id);
|
|
const memberCount = guild?.memberCount || server.member_count || 0;
|
|
|
|
// Update member count in database
|
|
if (guild && memberCount !== server.member_count) {
|
|
await supabaseClient
|
|
.from('federation_servers')
|
|
.update({ member_count: memberCount })
|
|
.eq('guild_id', server.guild_id);
|
|
}
|
|
|
|
// Calculate new trust level
|
|
const serverData = { ...server, member_count: memberCount };
|
|
const newLevel = calculateTrustLevel(serverData);
|
|
const currentLevel = server.trust_level || 'bronze';
|
|
|
|
if (newLevel !== currentLevel) {
|
|
const oldInfo = getTrustLevelInfo(currentLevel);
|
|
const newInfo = getTrustLevelInfo(newLevel);
|
|
|
|
await supabaseClient
|
|
.from('federation_servers')
|
|
.update({
|
|
trust_level: newLevel,
|
|
updated_at: new Date().toISOString(),
|
|
last_activity: new Date().toISOString()
|
|
})
|
|
.eq('guild_id', server.guild_id);
|
|
|
|
// Determine if upgrade or downgrade
|
|
const levels = ['bronze', 'silver', 'gold', 'platinum'];
|
|
const isUpgrade = levels.indexOf(newLevel) > levels.indexOf(currentLevel);
|
|
|
|
if (isUpgrade) {
|
|
upgrades++;
|
|
console.log(`[Federation] ${server.guild_name}: ${oldInfo.emoji} ${oldInfo.name} → ${newInfo.emoji} ${newInfo.name} (UPGRADE)`);
|
|
} else {
|
|
downgrades++;
|
|
console.log(`[Federation] ${server.guild_name}: ${oldInfo.emoji} ${oldInfo.name} → ${newInfo.emoji} ${newInfo.name} (DOWNGRADE)`);
|
|
}
|
|
|
|
// Send notification to the server if we can
|
|
if (guild) {
|
|
try {
|
|
const systemChannel = guild.systemChannel;
|
|
if (systemChannel) {
|
|
const { EmbedBuilder } = require('discord.js');
|
|
const embed = new EmbedBuilder()
|
|
.setColor(newInfo.color)
|
|
.setTitle(`${newInfo.emoji} Federation Trust Level ${isUpgrade ? 'Upgrade' : 'Change'}!`)
|
|
.setDescription(isUpgrade
|
|
? `Congratulations! Your server has been promoted to **${newInfo.name}** tier in the Federation.`
|
|
: `Your server's Federation tier has been adjusted to **${newInfo.name}**.`)
|
|
.addFields(
|
|
{ name: 'Previous Tier', value: `${oldInfo.emoji} ${oldInfo.name}`, inline: true },
|
|
{ name: 'New Tier', value: `${newInfo.emoji} ${newInfo.name}`, inline: true }
|
|
)
|
|
.setTimestamp();
|
|
|
|
await systemChannel.send({ embeds: [embed] }).catch(() => {});
|
|
}
|
|
} catch (e) {
|
|
// Silent fail for notifications
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (upgrades > 0 || downgrades > 0) {
|
|
console.log(`[Federation] Trust evaluation complete: ${upgrades} upgrades, ${downgrades} downgrades`);
|
|
}
|
|
} catch (error) {
|
|
console.error('[Federation] Trust evaluation error:', error.message);
|
|
}
|
|
}, 24 * 60 * 60 * 1000); // Evaluate daily
|
|
|
|
// Also run immediately on startup (after a short delay)
|
|
setTimeout(async () => {
|
|
console.log('[Federation] Running initial trust level evaluation...');
|
|
try {
|
|
const { data: servers } = await supabaseClient
|
|
.from('federation_servers')
|
|
.select('*')
|
|
.eq('status', 'approved');
|
|
|
|
console.log(`[Federation] Monitoring ${servers?.length || 0} federation servers`);
|
|
} catch (e) {
|
|
console.error('[Federation] Initial check error:', e.message);
|
|
}
|
|
}, 10000);
|
|
|
|
console.log('[Federation] Trust evaluator started - checking daily');
|
|
}
|
|
|
|
// =============================================================================
|
|
// ERROR HANDLING
|
|
// =============================================================================
|
|
|
|
process.on("unhandledRejection", (error) => {
|
|
console.error("Unhandled Promise Rejection:", error);
|
|
});
|
|
|
|
process.on("uncaughtException", (error) => {
|
|
console.error("Uncaught Exception:", error);
|
|
process.exit(1);
|
|
});
|
|
|
|
module.exports = client;
|