Add Sentinel anti-nuke listeners, federation role syncing, ticket system, and admin commands to the unified AeThex bot, consolidating functionality and enhancing security monitoring. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e72fc1b7-94bd-4d6c-801f-cbac2fae245c Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 00c4494a-b436-4e48-b794-39cd745fb604 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/e72fc1b7-94bd-4d6c-801f-cbac2fae245c/7DQc4BR Replit-Helium-Checkpoint-Created: true
263 lines
7.7 KiB
JavaScript
263 lines
7.7 KiB
JavaScript
const {
|
|
Client,
|
|
GatewayIntentBits,
|
|
REST,
|
|
Routes,
|
|
Collection,
|
|
EmbedBuilder,
|
|
ChannelType,
|
|
PermissionFlagsBits,
|
|
AuditLogEvent,
|
|
} = require("discord.js");
|
|
const { createClient } = require("@supabase/supabase-js");
|
|
const http = require("http");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
require("dotenv").config();
|
|
|
|
const token = process.env.DISCORD_BOT_TOKEN || process.env.DISCORD_TOKEN;
|
|
const clientId = process.env.DISCORD_CLIENT_ID;
|
|
|
|
if (!token) {
|
|
console.error("Missing DISCORD_BOT_TOKEN or DISCORD_TOKEN");
|
|
process.exit(1);
|
|
}
|
|
|
|
const client = new Client({
|
|
intents: [
|
|
GatewayIntentBits.Guilds,
|
|
GatewayIntentBits.GuildMembers,
|
|
GatewayIntentBits.GuildModeration,
|
|
GatewayIntentBits.GuildMessages,
|
|
GatewayIntentBits.MessageContent,
|
|
GatewayIntentBits.DirectMessages,
|
|
],
|
|
});
|
|
|
|
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");
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const heatMap = new Map();
|
|
const HEAT_THRESHOLD = 3;
|
|
const HEAT_WINDOW_MS = 10000;
|
|
const DANGEROUS_ACTIONS = ['CHANNEL_DELETE', 'ROLE_DELETE', 'MEMBER_BAN_ADD', 'MEMBER_KICK'];
|
|
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;
|
|
|
|
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;
|
|
|
|
const activeTickets = new Map();
|
|
client.activeTickets = activeTickets;
|
|
|
|
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;
|
|
|
|
client.once("ready", () => {
|
|
console.log(`Bot logged in as ${client.user.tag}`);
|
|
console.log(`Watching ${client.guilds.cache.size} server(s)`);
|
|
|
|
client.user.setActivity("Protecting the Federation", { type: 3 });
|
|
|
|
sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`);
|
|
});
|
|
|
|
client.on("interactionCreate", async (interaction) => {
|
|
if (interaction.isChatInputCommand()) {
|
|
const command = client.commands.get(interaction.commandName);
|
|
if (!command) 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.");
|
|
|
|
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') {
|
|
const ticketChannelId = params[1];
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const healthPort = process.env.HEALTH_PORT || 8080;
|
|
http.createServer((req, res) => {
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
res.setHeader("Content-Type", "application/json");
|
|
|
|
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,
|
|
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;
|
|
}
|
|
|
|
res.writeHead(404);
|
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
}).listen(healthPort, () => {
|
|
console.log(`Health server running on port ${healthPort}`);
|
|
});
|
|
|
|
client.login(token).catch((error) => {
|
|
console.error("Failed to login:", error.message);
|
|
process.exit(1);
|
|
});
|
|
|
|
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;
|