AeThex-Bot-Master/aethex-bot/bot.js
sirpiglr b178664f99 Make Supabase features optional and integrate new security systems
Updates bot.js to make Supabase integration optional, adds Sentinel security listeners, and modifies several commands to handle missing Supabase configurations gracefully. Also updates package.json and replit.md for new dependencies and features.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 0d645005-4840-49ef-9446-2c62d2bb7eed
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/Wmps8l5
Replit-Helium-Checkpoint-Created: true
2025-12-07 23:41:11 +00:00

984 lines
31 KiB
JavaScript

const {
Client,
GatewayIntentBits,
REST,
Routes,
Collection,
EmbedBuilder,
ChannelType,
PermissionFlagsBits,
} = require("discord.js");
const { createClient } = require("@supabase/supabase-js");
const http = require("http");
const fs = require("fs");
const path = require("path");
require("dotenv").config();
// =============================================================================
// ENVIRONMENT VALIDATION (Modified: Supabase now optional)
// =============================================================================
const token = process.env.DISCORD_BOT_TOKEN;
const clientId = process.env.DISCORD_CLIENT_ID;
if (!token) {
console.error("Missing DISCORD_BOT_TOKEN environment variable");
process.exit(1);
}
if (!clientId) {
console.error("Missing DISCORD_CLIENT_ID environment variable");
process.exit(1);
}
console.log("[Token] Bot token loaded (length: " + token.length + " chars)");
// =============================================================================
// DISCORD CLIENT SETUP (Modified: Added intents for Sentinel)
// =============================================================================
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildModeration,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages,
],
});
// =============================================================================
// SUPABASE SETUP (Modified: Now optional)
// =============================================================================
let supabase = null;
if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) {
supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE
);
console.log("Supabase connected");
} else {
console.log("Supabase not configured - community features will be limited");
}
// =============================================================================
// 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;
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;
// =============================================================================
// SENTINEL: TICKET TRACKING (New)
// =============================================================================
const activeTickets = new Map();
client.activeTickets = activeTickets;
// =============================================================================
// 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;
// =============================================================================
// 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}`);
}
}
}
// =============================================================================
// 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) => {
if (interaction.isChatInputCommand()) {
const command = client.commands.get(interaction.commandName);
if (!command) {
console.warn(`No command matching ${interaction.commandName} was found.`);
return;
}
try {
await command.execute(interaction, supabase, client);
} catch (error) {
console.error(`Error executing ${interaction.commandName}:`, error);
const errorEmbed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("Command Error")
.setDescription("There was an error while executing this command.")
.setFooter({ text: "Contact support if this persists" });
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ embeds: [errorEmbed], ephemeral: true });
} else {
await interaction.reply({ embeds: [errorEmbed], ephemeral: true });
}
}
}
if (interaction.isButton()) {
const [action, ...params] = interaction.customId.split('_');
if (action === 'ticket') {
const ticketAction = params[0];
if (ticketAction === 'close') {
try {
const channel = interaction.channel;
if (channel && channel.type === ChannelType.GuildText) {
await interaction.reply({ content: 'Closing ticket...', ephemeral: true });
setTimeout(() => channel.delete().catch(console.error), 3000);
}
} catch (err) {
console.error('Ticket close error:', err);
}
}
}
}
});
// =============================================================================
// COMMANDS FOR REGISTRATION (Modified: Added Sentinel commands)
// =============================================================================
const COMMANDS_TO_REGISTER = [
{
name: "verify",
description: "Link your Discord account to AeThex",
},
{
name: "set-realm",
description: "Choose your primary arm/realm (Labs, GameForge, Corp, etc.)",
options: [
{
name: "realm",
type: 3,
description: "Your primary realm",
required: true,
choices: [
{ name: "Labs", value: "labs" },
{ name: "GameForge", value: "gameforge" },
{ name: "Corp", value: "corp" },
{ name: "Foundation", value: "foundation" },
{ name: "Dev-Link", value: "devlink" },
],
},
],
},
{
name: "profile",
description: "View your linked AeThex profile",
},
{
name: "unlink",
description: "Disconnect your Discord account from AeThex",
},
{
name: "verify-role",
description: "Check your assigned Discord roles",
},
{
name: "help",
description: "View all AeThex bot commands and features",
},
{
name: "stats",
description: "View your AeThex statistics and activity",
},
{
name: "leaderboard",
description: "View the top AeThex contributors",
options: [
{
name: "category",
type: 3,
description: "Leaderboard category",
required: false,
choices: [
{ name: "Most Active (Posts)", value: "posts" },
{ name: "Most Liked", value: "likes" },
{ name: "Top Creators", value: "creators" },
],
},
],
},
{
name: "post",
description: "Create a post in the AeThex community feed",
options: [
{
name: "content",
type: 3,
description: "Your post content",
required: true,
max_length: 500,
},
{
name: "category",
type: 3,
description: "Post category",
required: false,
choices: [
{ name: "General", value: "general" },
{ name: "Project Update", value: "project_update" },
{ name: "Question", value: "question" },
{ name: "Idea", value: "idea" },
{ name: "Announcement", value: "announcement" },
],
},
{
name: "image",
type: 11,
description: "Attach an image to your post",
required: false,
},
],
},
{
name: "refresh-roles",
description: "Refresh your Discord roles based on your AeThex profile",
},
// Sentinel Commands
{
name: "admin",
description: "Admin controls for bot management",
options: [
{
name: "action",
type: 3,
description: "Admin action to perform",
required: true,
choices: [
{ name: "Status", value: "status" },
{ name: "Heat Check", value: "heat" },
{ name: "Servers", value: "servers" },
{ name: "Threats", value: "threats" },
{ name: "Federation", value: "federation" },
],
},
{
name: "user",
type: 6,
description: "Target user (for heat check)",
required: false,
},
],
},
{
name: "federation",
description: "Manage federation role sync",
options: [
{
name: "action",
type: 3,
description: "Federation action",
required: true,
choices: [
{ name: "Link Role", value: "link" },
{ name: "Unlink Role", value: "unlink" },
{ name: "List Linked", value: "list" },
],
},
{
name: "role",
type: 8,
description: "Role to link/unlink",
required: false,
},
],
},
{
name: "status",
description: "View network status and bot information",
},
{
name: "ticket",
description: "Create or close support tickets",
options: [
{
name: "action",
type: 3,
description: "Ticket action",
required: true,
choices: [
{ name: "Create", value: "create" },
{ name: "Close", value: "close" },
],
},
{
name: "reason",
type: 3,
description: "Reason for ticket (when creating)",
required: false,
},
],
},
];
// =============================================================================
// COMMAND REGISTRATION FUNCTION
// =============================================================================
async function registerDiscordCommands() {
try {
const rest = new REST({ version: "10" }).setToken(
process.env.DISCORD_BOT_TOKEN,
);
console.log(
`Registering ${COMMANDS_TO_REGISTER.length} slash commands...`,
);
try {
const data = await rest.put(
Routes.applicationCommands(process.env.DISCORD_CLIENT_ID),
{ body: COMMANDS_TO_REGISTER },
);
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 COMMANDS_TO_REGISTER) {
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}`;
};
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 === "/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,
}));
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 stats = {
commands: COMMANDS_TO_REGISTER.map((cmd) => ({
name: cmd.name,
description: cmd.description,
options: cmd.options?.length || 0,
})),
totalCommands: COMMANDS_TO_REGISTER.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 === "/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 ${COMMANDS_TO_REGISTER.length} 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;
}
}
res.writeHead(404);
res.end(JSON.stringify({ error: "Not found" }));
})
.listen(healthPort, () => {
console.log(`Health check server running on port ${healthPort}`);
console.log(`Register commands at: POST http://localhost:${healthPort}/register-commands`);
});
// =============================================================================
// BOT LOGIN AND READY
// =============================================================================
client.login(token).catch((error) => {
console.error("Failed to login to Discord");
console.error(`Error Code: ${error.code}`);
console.error(`Error Message: ${error.message}`);
if (error.code === "TokenInvalid") {
console.error("\nDISCORD_BOT_TOKEN is invalid!");
console.error("Get a new token from: https://discord.com/developers/applications");
}
process.exit(1);
});
client.once("ready", () => {
console.log(`Bot logged in as ${client.user.tag}`);
console.log(`Watching ${client.guilds.cache.size} server(s)`);
console.log("Commands are registered via: npm run register-commands");
client.user.setActivity("Protecting the Federation", { type: 3 });
if (setupFeedListener && supabase) {
setupFeedListener(client);
}
sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`);
});
// =============================================================================
// ERROR HANDLING
// =============================================================================
process.on("unhandledRejection", (error) => {
console.error("Unhandled Promise Rejection:", error);
});
process.on("uncaughtException", (error) => {
console.error("Uncaught Exception:", error);
process.exit(1);
});
module.exports = client;