Integrate security features and administration tools into the main bot
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
This commit is contained in:
parent
eaffacca6b
commit
ddea985e6f
15 changed files with 2281 additions and 19 deletions
27
.replit
27
.replit
|
|
@ -32,32 +32,21 @@ author = "agent"
|
||||||
|
|
||||||
[[workflows.workflow.tasks]]
|
[[workflows.workflow.tasks]]
|
||||||
task = "workflow.run"
|
task = "workflow.run"
|
||||||
args = "Bot Master Dashboard"
|
args = "AeThex Unified Bot"
|
||||||
|
|
||||||
[[workflows.workflow.tasks]]
|
|
||||||
task = "workflow.run"
|
|
||||||
args = "Aethex Sentinel Bot"
|
|
||||||
|
|
||||||
[[workflows.workflow]]
|
[[workflows.workflow]]
|
||||||
name = "Bot Master Dashboard"
|
name = "AeThex Unified Bot"
|
||||||
author = "agent"
|
author = "agent"
|
||||||
|
|
||||||
[[workflows.workflow.tasks]]
|
[[workflows.workflow.tasks]]
|
||||||
task = "shell.exec"
|
task = "shell.exec"
|
||||||
args = "python main.py"
|
args = "cd aethex-bot && npm start"
|
||||||
waitForPort = 5000
|
|
||||||
|
|
||||||
[workflows.workflow.metadata]
|
|
||||||
outputType = "webview"
|
|
||||||
|
|
||||||
[[workflows.workflow]]
|
|
||||||
name = "Aethex Sentinel Bot"
|
|
||||||
author = "agent"
|
|
||||||
|
|
||||||
[[workflows.workflow.tasks]]
|
|
||||||
task = "shell.exec"
|
|
||||||
args = "cd sentinel-bot && npm start"
|
|
||||||
waitForPort = 8080
|
waitForPort = 8080
|
||||||
|
|
||||||
[workflows.workflow.metadata]
|
[workflows.workflow.metadata]
|
||||||
outputType = "console"
|
outputType = "console"
|
||||||
|
|
||||||
|
[userenv]
|
||||||
|
|
||||||
|
[userenv.shared]
|
||||||
|
DISCORD_CLIENT_ID = "1447339527885553828"
|
||||||
|
|
|
||||||
21
aethex-bot/.env.example
Normal file
21
aethex-bot/.env.example
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Required
|
||||||
|
DISCORD_BOT_TOKEN=your_discord_bot_token
|
||||||
|
DISCORD_CLIENT_ID=your_discord_client_id
|
||||||
|
|
||||||
|
# Optional - Supabase (for user verification features)
|
||||||
|
SUPABASE_URL=your_supabase_url
|
||||||
|
SUPABASE_SERVICE_ROLE=your_supabase_service_role_key
|
||||||
|
|
||||||
|
# Optional - Federation Guild IDs
|
||||||
|
HUB_GUILD_ID=main_hub_server_id
|
||||||
|
LABS_GUILD_ID=labs_server_id
|
||||||
|
GAMEFORGE_GUILD_ID=gameforge_server_id
|
||||||
|
CORP_GUILD_ID=corp_server_id
|
||||||
|
FOUNDATION_GUILD_ID=foundation_server_id
|
||||||
|
|
||||||
|
# Optional - Security
|
||||||
|
WHITELISTED_USERS=user_id_1,user_id_2
|
||||||
|
ALERT_CHANNEL_ID=channel_id_for_alerts
|
||||||
|
|
||||||
|
# Optional - Health server
|
||||||
|
HEALTH_PORT=8080
|
||||||
263
aethex-bot/bot.js
Normal file
263
aethex-bot/bot.js
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
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;
|
||||||
157
aethex-bot/commands/admin.js
Normal file
157
aethex-bot/commands/admin.js
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('admin')
|
||||||
|
.setDescription('Admin monitoring commands')
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('status')
|
||||||
|
.setDescription('View bot status and statistics')
|
||||||
|
)
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('heat')
|
||||||
|
.setDescription('Check heat level of a user')
|
||||||
|
.addUserOption(option =>
|
||||||
|
option.setName('user')
|
||||||
|
.setDescription('User to check')
|
||||||
|
.setRequired(true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('servers')
|
||||||
|
.setDescription('View all servers the bot is in')
|
||||||
|
)
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('threats')
|
||||||
|
.setDescription('View current heat map (active threats)')
|
||||||
|
)
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('federation')
|
||||||
|
.setDescription('View federation role mappings')
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(interaction, supabase, client) {
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
|
if (subcommand === 'status') {
|
||||||
|
const guildCount = client.guilds.cache.size;
|
||||||
|
const memberCount = client.guilds.cache.reduce((sum, g) => sum + g.memberCount, 0);
|
||||||
|
const commandCount = client.commands.size;
|
||||||
|
const uptime = Math.floor(process.uptime());
|
||||||
|
const hours = Math.floor(uptime / 3600);
|
||||||
|
const minutes = Math.floor((uptime % 3600) / 60);
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0x7c3aed)
|
||||||
|
.setTitle('AeThex Bot Status')
|
||||||
|
.setThumbnail(client.user.displayAvatarURL())
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Servers', value: `${guildCount}`, inline: true },
|
||||||
|
{ name: 'Total Members', value: `${memberCount.toLocaleString()}`, inline: true },
|
||||||
|
{ name: 'Commands', value: `${commandCount}`, inline: true },
|
||||||
|
{ name: 'Uptime', value: `${hours}h ${minutes}m`, inline: true },
|
||||||
|
{ name: 'Active Tickets', value: `${client.activeTickets.size}`, inline: true },
|
||||||
|
{ name: 'Heat Map Size', value: `${client.heatMap.size}`, inline: true }
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'heat') {
|
||||||
|
const user = interaction.options.getUser('user');
|
||||||
|
const heat = client.getHeat(user.id);
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(heat >= client.HEAT_THRESHOLD ? 0xff0000 : heat > 0 ? 0xffaa00 : 0x00ff00)
|
||||||
|
.setTitle('User Heat Level')
|
||||||
|
.setThumbnail(user.displayAvatarURL())
|
||||||
|
.addFields(
|
||||||
|
{ name: 'User', value: user.tag, inline: true },
|
||||||
|
{ name: 'Heat Level', value: `${heat}/${client.HEAT_THRESHOLD}`, inline: true },
|
||||||
|
{ name: 'Status', value: heat >= client.HEAT_THRESHOLD ? 'DANGER' : heat > 0 ? 'Elevated' : 'Normal', inline: true }
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'servers') {
|
||||||
|
const guilds = client.guilds.cache.map(g => `**${g.name}** - ${g.memberCount.toLocaleString()} members`).join('\n');
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0x7c3aed)
|
||||||
|
.setTitle('Connected Servers')
|
||||||
|
.setDescription(guilds || 'No servers')
|
||||||
|
.setFooter({ text: `Total: ${client.guilds.cache.size} servers` })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'threats') {
|
||||||
|
const now = Date.now();
|
||||||
|
const activeThreats = [];
|
||||||
|
|
||||||
|
for (const [userId, events] of client.heatMap) {
|
||||||
|
const recentEvents = events.filter(e => now - e.timestamp < 10000);
|
||||||
|
if (recentEvents.length > 0) {
|
||||||
|
try {
|
||||||
|
const user = await client.users.fetch(userId).catch(() => null);
|
||||||
|
activeThreats.push({
|
||||||
|
user: user ? user.tag : userId,
|
||||||
|
heat: recentEvents.length,
|
||||||
|
actions: recentEvents.map(e => e.action).join(', ')
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
activeThreats.push({
|
||||||
|
user: userId,
|
||||||
|
heat: recentEvents.length,
|
||||||
|
actions: recentEvents.map(e => e.action).join(', ')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const description = activeThreats.length > 0
|
||||||
|
? activeThreats.map(t => `**${t.user}** - Heat: ${t.heat} (${t.actions})`).join('\n')
|
||||||
|
: 'No active threats detected.';
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(activeThreats.length > 0 ? 0xff0000 : 0x00ff00)
|
||||||
|
.setTitle('Active Threat Monitor')
|
||||||
|
.setDescription(description)
|
||||||
|
.setFooter({ text: `Heat threshold: ${client.HEAT_THRESHOLD}` })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'federation') {
|
||||||
|
const mappings = [...client.federationMappings.entries()];
|
||||||
|
|
||||||
|
const description = mappings.length > 0
|
||||||
|
? mappings.map(([roleId, data]) => `<@&${roleId}> - Synced across realms`).join('\n')
|
||||||
|
: 'No federation role mappings configured.\nUse `/federation link` to set up cross-server roles.';
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0x7c3aed)
|
||||||
|
.setTitle('Federation Role Mappings')
|
||||||
|
.setDescription(description)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Hub Guild', value: client.REALM_GUILDS.hub || 'Not set', inline: true },
|
||||||
|
{ name: 'Labs Guild', value: client.REALM_GUILDS.labs || 'Not set', inline: true },
|
||||||
|
{ name: 'GameForge Guild', value: client.REALM_GUILDS.gameforge || 'Not set', inline: true }
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
114
aethex-bot/commands/federation.js
Normal file
114
aethex-bot/commands/federation.js
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('federation')
|
||||||
|
.setDescription('Manage cross-server role sync')
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('link')
|
||||||
|
.setDescription('Link a role for cross-server syncing')
|
||||||
|
.addRoleOption(option =>
|
||||||
|
option.setName('role')
|
||||||
|
.setDescription('Role to sync across realms')
|
||||||
|
.setRequired(true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('unlink')
|
||||||
|
.setDescription('Remove a role from cross-server syncing')
|
||||||
|
.addRoleOption(option =>
|
||||||
|
option.setName('role')
|
||||||
|
.setDescription('Role to remove from sync')
|
||||||
|
.setRequired(true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('list')
|
||||||
|
.setDescription('List all linked roles')
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(interaction, supabase, client) {
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
|
if (subcommand === 'link') {
|
||||||
|
const role = interaction.options.getRole('role');
|
||||||
|
|
||||||
|
if (client.federationMappings.has(role.id)) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: `${role} is already linked for federation sync.`,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
client.federationMappings.set(role.id, {
|
||||||
|
name: role.name,
|
||||||
|
guildId: interaction.guild.id,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0x00ff00)
|
||||||
|
.setTitle('Role Linked')
|
||||||
|
.setDescription(`${role} will now sync across all realm servers.`)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Role Name', value: role.name, inline: true },
|
||||||
|
{ name: 'Source Guild', value: interaction.guild.name, inline: true }
|
||||||
|
)
|
||||||
|
.setFooter({ text: 'Users with this role will receive it in all connected realms.' })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed] });
|
||||||
|
|
||||||
|
client.sendAlert(`Federation: Role ${role.name} linked by ${interaction.user.tag}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'unlink') {
|
||||||
|
const role = interaction.options.getRole('role');
|
||||||
|
|
||||||
|
if (!client.federationMappings.has(role.id)) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: `${role} is not linked for federation sync.`,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
client.federationMappings.delete(role.id);
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0xff0000)
|
||||||
|
.setTitle('Role Unlinked')
|
||||||
|
.setDescription(`${role} will no longer sync across realm servers.`)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed] });
|
||||||
|
|
||||||
|
client.sendAlert(`Federation: Role ${role.name} unlinked by ${interaction.user.tag}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'list') {
|
||||||
|
const mappings = [...client.federationMappings.entries()];
|
||||||
|
|
||||||
|
if (mappings.length === 0) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: 'No roles are currently linked for federation sync.',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleList = mappings.map(([roleId, data]) => `<@&${roleId}> - Added <t:${Math.floor(data.createdAt / 1000)}:R>`).join('\n');
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0x7c3aed)
|
||||||
|
.setTitle('Federation Linked Roles')
|
||||||
|
.setDescription(roleList)
|
||||||
|
.setFooter({ text: `${mappings.length} role(s) linked` })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
57
aethex-bot/commands/status.js
Normal file
57
aethex-bot/commands/status.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('status')
|
||||||
|
.setDescription('View network status and bot health'),
|
||||||
|
|
||||||
|
async execute(interaction, supabase, client) {
|
||||||
|
const guildCount = client.guilds.cache.size;
|
||||||
|
const memberCount = client.guilds.cache.reduce((sum, g) => sum + g.memberCount, 0);
|
||||||
|
const uptime = Math.floor(process.uptime());
|
||||||
|
const hours = Math.floor(uptime / 3600);
|
||||||
|
const minutes = Math.floor((uptime % 3600) / 60);
|
||||||
|
const seconds = uptime % 60;
|
||||||
|
|
||||||
|
const realmStatus = [];
|
||||||
|
const REALM_GUILDS = client.REALM_GUILDS;
|
||||||
|
|
||||||
|
for (const [realm, guildId] of Object.entries(REALM_GUILDS)) {
|
||||||
|
if (!guildId) {
|
||||||
|
realmStatus.push({ name: realm.charAt(0).toUpperCase() + realm.slice(1), status: 'Not configured', members: 0 });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = client.guilds.cache.get(guildId);
|
||||||
|
if (guild) {
|
||||||
|
realmStatus.push({ name: realm.charAt(0).toUpperCase() + realm.slice(1), status: 'Online', members: guild.memberCount });
|
||||||
|
} else {
|
||||||
|
realmStatus.push({ name: realm.charAt(0).toUpperCase() + realm.slice(1), status: 'Offline', members: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const realmFields = realmStatus.map(r => ({
|
||||||
|
name: r.name,
|
||||||
|
value: `${r.status === 'Online' ? '🟢' : r.status === 'Offline' ? '🔴' : '⚪'} ${r.status}${r.members > 0 ? ` (${r.members.toLocaleString()})` : ''}`,
|
||||||
|
inline: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0x7c3aed)
|
||||||
|
.setTitle('AeThex Network Status')
|
||||||
|
.setDescription('Current status of the AeThex Federation')
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Total Servers', value: `${guildCount}`, inline: true },
|
||||||
|
{ name: 'Total Members', value: `${memberCount.toLocaleString()}`, inline: true },
|
||||||
|
{ name: 'Uptime', value: `${hours}h ${minutes}m ${seconds}s`, inline: true },
|
||||||
|
...realmFields,
|
||||||
|
{ name: 'Sentinel Status', value: client.heatMap.size > 0 ? `⚠️ Monitoring ${client.heatMap.size} user(s)` : '🛡️ All Clear', inline: false },
|
||||||
|
{ name: 'Active Tickets', value: `${client.activeTickets.size}`, inline: true },
|
||||||
|
{ name: 'Federation Mappings', value: `${client.federationMappings.size}`, inline: true }
|
||||||
|
)
|
||||||
|
.setFooter({ text: 'AeThex Unified Bot' })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
128
aethex-bot/commands/ticket.js
Normal file
128
aethex-bot/commands/ticket.js
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
const { SlashCommandBuilder, EmbedBuilder, ChannelType, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('ticket')
|
||||||
|
.setDescription('Ticket management system')
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('create')
|
||||||
|
.setDescription('Create a new support ticket')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('reason')
|
||||||
|
.setDescription('Brief reason for opening this ticket')
|
||||||
|
.setRequired(true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('close')
|
||||||
|
.setDescription('Close the current ticket')
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(interaction, supabase, client) {
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
|
if (subcommand === 'create') {
|
||||||
|
const reason = interaction.options.getString('reason');
|
||||||
|
const guild = interaction.guild;
|
||||||
|
const user = interaction.user;
|
||||||
|
|
||||||
|
const existingTicket = client.activeTickets.get(user.id);
|
||||||
|
if (existingTicket) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: `You already have an open ticket: <#${existingTicket}>`,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ticketChannel = await guild.channels.create({
|
||||||
|
name: `ticket-${user.username}`,
|
||||||
|
type: ChannelType.GuildText,
|
||||||
|
permissionOverwrites: [
|
||||||
|
{
|
||||||
|
id: guild.id,
|
||||||
|
deny: [PermissionFlagsBits.ViewChannel],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: user.id,
|
||||||
|
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: client.user.id,
|
||||||
|
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ManageChannels],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
client.activeTickets.set(user.id, ticketChannel.id);
|
||||||
|
|
||||||
|
const closeButton = new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId(`ticket_close_${ticketChannel.id}`)
|
||||||
|
.setLabel('Close Ticket')
|
||||||
|
.setStyle(ButtonStyle.Danger)
|
||||||
|
);
|
||||||
|
|
||||||
|
const ticketEmbed = new EmbedBuilder()
|
||||||
|
.setColor(0x7c3aed)
|
||||||
|
.setTitle('Support Ticket')
|
||||||
|
.setDescription(`Ticket created by ${user}`)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Reason', value: reason },
|
||||||
|
{ name: 'User ID', value: user.id, inline: true },
|
||||||
|
{ name: 'Created', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
|
||||||
|
)
|
||||||
|
.setFooter({ text: 'A staff member will assist you shortly.' });
|
||||||
|
|
||||||
|
await ticketChannel.send({ embeds: [ticketEmbed], components: [closeButton] });
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Ticket created: ${ticketChannel}`,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.sendAlert(`New ticket opened by ${user.tag}: ${reason}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Ticket] Create error:', error);
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'Failed to create ticket. Please try again.',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'close') {
|
||||||
|
const channel = interaction.channel;
|
||||||
|
|
||||||
|
if (!channel.name.startsWith('ticket-')) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: 'This command can only be used in ticket channels.',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = [...client.activeTickets.entries()].find(([k, v]) => v === channel.id)?.[0];
|
||||||
|
if (userId) {
|
||||||
|
client.activeTickets.delete(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'Closing ticket in 5 seconds...',
|
||||||
|
});
|
||||||
|
|
||||||
|
client.sendAlert(`Ticket ${channel.name} closed by ${interaction.user.tag}`);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await channel.delete();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Ticket] Delete error:', err);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
80
aethex-bot/events/guildMemberUpdate.js
Normal file
80
aethex-bot/events/guildMemberUpdate.js
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
const { EmbedBuilder } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'guildMemberUpdate',
|
||||||
|
async execute(oldMember, newMember, client) {
|
||||||
|
try {
|
||||||
|
const addedRoles = newMember.roles.cache.filter(role => !oldMember.roles.cache.has(role.id));
|
||||||
|
const removedRoles = oldMember.roles.cache.filter(role => !newMember.roles.cache.has(role.id));
|
||||||
|
|
||||||
|
if (addedRoles.size === 0 && removedRoles.size === 0) return;
|
||||||
|
|
||||||
|
const REALM_GUILDS = client.REALM_GUILDS;
|
||||||
|
const realmGuildIds = Object.values(REALM_GUILDS).filter(Boolean);
|
||||||
|
|
||||||
|
if (!realmGuildIds.includes(newMember.guild.id)) return;
|
||||||
|
|
||||||
|
const federationMappings = client.federationMappings;
|
||||||
|
|
||||||
|
for (const [roleId, role] of addedRoles) {
|
||||||
|
const mapping = federationMappings.get(roleId);
|
||||||
|
if (!mapping) continue;
|
||||||
|
|
||||||
|
console.log(`[Federation] Syncing role ${role.name} for ${newMember.user.tag} across realms`);
|
||||||
|
|
||||||
|
for (const targetGuildId of realmGuildIds) {
|
||||||
|
if (targetGuildId === newMember.guild.id) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const targetGuild = client.guilds.cache.get(targetGuildId);
|
||||||
|
if (!targetGuild) continue;
|
||||||
|
|
||||||
|
const targetMember = await targetGuild.members.fetch(newMember.user.id).catch(() => null);
|
||||||
|
if (!targetMember) continue;
|
||||||
|
|
||||||
|
const targetRole = targetGuild.roles.cache.find(r => r.name === role.name);
|
||||||
|
if (!targetRole) continue;
|
||||||
|
|
||||||
|
if (!targetMember.roles.cache.has(targetRole.id)) {
|
||||||
|
await targetMember.roles.add(targetRole, '[Federation] Cross-server sync');
|
||||||
|
console.log(`[Federation] Added ${role.name} to ${newMember.user.tag} in ${targetGuild.name}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Federation] Sync error for guild ${targetGuildId}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [roleId, role] of removedRoles) {
|
||||||
|
const mapping = federationMappings.get(roleId);
|
||||||
|
if (!mapping) continue;
|
||||||
|
|
||||||
|
console.log(`[Federation] Removing synced role ${role.name} for ${newMember.user.tag} across realms`);
|
||||||
|
|
||||||
|
for (const targetGuildId of realmGuildIds) {
|
||||||
|
if (targetGuildId === newMember.guild.id) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const targetGuild = client.guilds.cache.get(targetGuildId);
|
||||||
|
if (!targetGuild) continue;
|
||||||
|
|
||||||
|
const targetMember = await targetGuild.members.fetch(newMember.user.id).catch(() => null);
|
||||||
|
if (!targetMember) continue;
|
||||||
|
|
||||||
|
const targetRole = targetGuild.roles.cache.find(r => r.name === role.name);
|
||||||
|
if (!targetRole) continue;
|
||||||
|
|
||||||
|
if (targetMember.roles.cache.has(targetRole.id)) {
|
||||||
|
await targetMember.roles.remove(targetRole, '[Federation] Cross-server sync');
|
||||||
|
console.log(`[Federation] Removed ${role.name} from ${newMember.user.tag} in ${targetGuild.name}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Federation] Sync error for guild ${targetGuildId}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Federation] guildMemberUpdate error:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
55
aethex-bot/listeners/sentinel/antiNuke.js
Normal file
55
aethex-bot/listeners/sentinel/antiNuke.js
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
const { EmbedBuilder, AuditLogEvent } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'channelDelete',
|
||||||
|
async execute(channel, client) {
|
||||||
|
try {
|
||||||
|
const guild = channel.guild;
|
||||||
|
if (!guild) return;
|
||||||
|
|
||||||
|
const auditLogs = await guild.fetchAuditLogs({
|
||||||
|
type: AuditLogEvent.ChannelDelete,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const log = auditLogs.entries.first();
|
||||||
|
if (!log) return;
|
||||||
|
|
||||||
|
const { executor, target } = log;
|
||||||
|
if (!executor || executor.id === client.user.id) return;
|
||||||
|
|
||||||
|
const heat = client.addHeat(executor.id, 'CHANNEL_DELETE');
|
||||||
|
console.log(`[Sentinel] User ${executor.tag} deleted channel. Heat: ${heat}/${client.HEAT_THRESHOLD}`);
|
||||||
|
|
||||||
|
if (heat >= client.HEAT_THRESHOLD) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0xff0000)
|
||||||
|
.setTitle('ANTI-NUKE TRIGGERED')
|
||||||
|
.setDescription(`User **${executor.tag}** has been banned for deleting too many channels.`)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'User ID', value: executor.id, inline: true },
|
||||||
|
{ name: 'Heat Level', value: `${heat}/${client.HEAT_THRESHOLD}`, inline: true },
|
||||||
|
{ name: 'Action', value: 'Automatic Ban', inline: true }
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await guild.members.ban(executor.id, { reason: '[Sentinel] Anti-nuke: Mass channel deletion detected' });
|
||||||
|
console.log(`[Sentinel] BANNED ${executor.tag} for mass channel deletion`);
|
||||||
|
|
||||||
|
client.sendAlert(`ANTI-NUKE: Banned ${executor.tag} for mass channel deletion`, embed);
|
||||||
|
|
||||||
|
const owner = await guild.fetchOwner();
|
||||||
|
if (owner) {
|
||||||
|
await owner.send({ content: `[SENTINEL ALERT] User ${executor.tag} was banned for mass channel deletion in ${guild.name}`, embeds: [embed] }).catch(() => {});
|
||||||
|
}
|
||||||
|
} catch (banError) {
|
||||||
|
console.error('[Sentinel] Failed to ban user:', banError.message);
|
||||||
|
client.sendAlert(`ANTI-NUKE FAILED: Could not ban ${executor.tag} - ${banError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Sentinel] Channel delete handler error:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
53
aethex-bot/listeners/sentinel/memberBan.js
Normal file
53
aethex-bot/listeners/sentinel/memberBan.js
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
const { EmbedBuilder, AuditLogEvent } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'guildBanAdd',
|
||||||
|
async execute(ban, client) {
|
||||||
|
try {
|
||||||
|
const guild = ban.guild;
|
||||||
|
|
||||||
|
const auditLogs = await guild.fetchAuditLogs({
|
||||||
|
type: AuditLogEvent.MemberBanAdd,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const log = auditLogs.entries.first();
|
||||||
|
if (!log) return;
|
||||||
|
|
||||||
|
const { executor, target } = log;
|
||||||
|
if (!executor || executor.id === client.user.id) return;
|
||||||
|
|
||||||
|
const heat = client.addHeat(executor.id, 'MEMBER_BAN_ADD');
|
||||||
|
console.log(`[Sentinel] User ${executor.tag} banned ${target.tag}. Heat: ${heat}/${client.HEAT_THRESHOLD}`);
|
||||||
|
|
||||||
|
if (heat >= client.HEAT_THRESHOLD) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0xff0000)
|
||||||
|
.setTitle('ANTI-NUKE TRIGGERED')
|
||||||
|
.setDescription(`User **${executor.tag}** has been banned for mass banning members.`)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'User ID', value: executor.id, inline: true },
|
||||||
|
{ name: 'Heat Level', value: `${heat}/${client.HEAT_THRESHOLD}`, inline: true },
|
||||||
|
{ name: 'Action', value: 'Mass Ban Detected', inline: true }
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await guild.members.ban(executor.id, { reason: '[Sentinel] Anti-nuke: Mass banning detected' });
|
||||||
|
console.log(`[Sentinel] BANNED ${executor.tag} for mass banning`);
|
||||||
|
|
||||||
|
client.sendAlert(`ANTI-NUKE: Banned ${executor.tag} for mass banning`, embed);
|
||||||
|
|
||||||
|
const owner = await guild.fetchOwner();
|
||||||
|
if (owner) {
|
||||||
|
await owner.send({ content: `[SENTINEL ALERT] User ${executor.tag} was banned for mass banning in ${guild.name}`, embeds: [embed] }).catch(() => {});
|
||||||
|
}
|
||||||
|
} catch (banError) {
|
||||||
|
console.error('[Sentinel] Failed to ban user:', banError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Sentinel] Member ban handler error:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
56
aethex-bot/listeners/sentinel/memberKick.js
Normal file
56
aethex-bot/listeners/sentinel/memberKick.js
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
const { EmbedBuilder, AuditLogEvent } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'guildMemberRemove',
|
||||||
|
async execute(member, client) {
|
||||||
|
try {
|
||||||
|
const guild = member.guild;
|
||||||
|
|
||||||
|
const auditLogs = await guild.fetchAuditLogs({
|
||||||
|
type: AuditLogEvent.MemberKick,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const log = auditLogs.entries.first();
|
||||||
|
if (!log) return;
|
||||||
|
|
||||||
|
const { executor, target, createdTimestamp } = log;
|
||||||
|
if (!executor || executor.id === client.user.id) return;
|
||||||
|
|
||||||
|
if (Date.now() - createdTimestamp > 5000) return;
|
||||||
|
if (target.id !== member.id) return;
|
||||||
|
|
||||||
|
const heat = client.addHeat(executor.id, 'MEMBER_KICK');
|
||||||
|
console.log(`[Sentinel] User ${executor.tag} kicked ${member.user.tag}. Heat: ${heat}/${client.HEAT_THRESHOLD}`);
|
||||||
|
|
||||||
|
if (heat >= client.HEAT_THRESHOLD) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0xff0000)
|
||||||
|
.setTitle('ANTI-NUKE TRIGGERED')
|
||||||
|
.setDescription(`User **${executor.tag}** has been banned for mass kicking members.`)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'User ID', value: executor.id, inline: true },
|
||||||
|
{ name: 'Heat Level', value: `${heat}/${client.HEAT_THRESHOLD}`, inline: true },
|
||||||
|
{ name: 'Action', value: 'Mass Kick Detected', inline: true }
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await guild.members.ban(executor.id, { reason: '[Sentinel] Anti-nuke: Mass kicking detected' });
|
||||||
|
console.log(`[Sentinel] BANNED ${executor.tag} for mass kicking`);
|
||||||
|
|
||||||
|
client.sendAlert(`ANTI-NUKE: Banned ${executor.tag} for mass kicking`, embed);
|
||||||
|
|
||||||
|
const owner = await guild.fetchOwner();
|
||||||
|
if (owner) {
|
||||||
|
await owner.send({ content: `[SENTINEL ALERT] User ${executor.tag} was banned for mass kicking in ${guild.name}`, embeds: [embed] }).catch(() => {});
|
||||||
|
}
|
||||||
|
} catch (banError) {
|
||||||
|
console.error('[Sentinel] Failed to ban user:', banError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Sentinel] Member kick handler error:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
54
aethex-bot/listeners/sentinel/roleDelete.js
Normal file
54
aethex-bot/listeners/sentinel/roleDelete.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
const { EmbedBuilder, AuditLogEvent } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'roleDelete',
|
||||||
|
async execute(role, client) {
|
||||||
|
try {
|
||||||
|
const guild = role.guild;
|
||||||
|
if (!guild) return;
|
||||||
|
|
||||||
|
const auditLogs = await guild.fetchAuditLogs({
|
||||||
|
type: AuditLogEvent.RoleDelete,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const log = auditLogs.entries.first();
|
||||||
|
if (!log) return;
|
||||||
|
|
||||||
|
const { executor } = log;
|
||||||
|
if (!executor || executor.id === client.user.id) return;
|
||||||
|
|
||||||
|
const heat = client.addHeat(executor.id, 'ROLE_DELETE');
|
||||||
|
console.log(`[Sentinel] User ${executor.tag} deleted role ${role.name}. Heat: ${heat}/${client.HEAT_THRESHOLD}`);
|
||||||
|
|
||||||
|
if (heat >= client.HEAT_THRESHOLD) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0xff0000)
|
||||||
|
.setTitle('ANTI-NUKE TRIGGERED')
|
||||||
|
.setDescription(`User **${executor.tag}** has been banned for deleting too many roles.`)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'User ID', value: executor.id, inline: true },
|
||||||
|
{ name: 'Heat Level', value: `${heat}/${client.HEAT_THRESHOLD}`, inline: true },
|
||||||
|
{ name: 'Deleted Role', value: role.name, inline: true }
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await guild.members.ban(executor.id, { reason: '[Sentinel] Anti-nuke: Mass role deletion detected' });
|
||||||
|
console.log(`[Sentinel] BANNED ${executor.tag} for mass role deletion`);
|
||||||
|
|
||||||
|
client.sendAlert(`ANTI-NUKE: Banned ${executor.tag} for mass role deletion`, embed);
|
||||||
|
|
||||||
|
const owner = await guild.fetchOwner();
|
||||||
|
if (owner) {
|
||||||
|
await owner.send({ content: `[SENTINEL ALERT] User ${executor.tag} was banned for mass role deletion in ${guild.name}`, embeds: [embed] }).catch(() => {});
|
||||||
|
}
|
||||||
|
} catch (banError) {
|
||||||
|
console.error('[Sentinel] Failed to ban user:', banError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Sentinel] Role delete handler error:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
1082
aethex-bot/package-lock.json
generated
Normal file
1082
aethex-bot/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
24
aethex-bot/package.json
Normal file
24
aethex-bot/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "aethex-unified-bot",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "AeThex Unified Bot - Community features + Sentinel security",
|
||||||
|
"main": "bot.js",
|
||||||
|
"type": "commonjs",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node bot.js",
|
||||||
|
"dev": "nodemon bot.js",
|
||||||
|
"register-commands": "node scripts/register-commands.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/supabase-js": "^2.38.0",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"discord.js": "^14.13.0",
|
||||||
|
"dotenv": "^16.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
129
aethex-bot/scripts/register-commands.js
Normal file
129
aethex-bot/scripts/register-commands.js
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
const { REST, Routes } = require('discord.js');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const commands = [
|
||||||
|
{
|
||||||
|
name: 'ticket',
|
||||||
|
description: 'Ticket management system',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'create',
|
||||||
|
type: 1,
|
||||||
|
description: 'Create a new support ticket',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'reason',
|
||||||
|
type: 3,
|
||||||
|
description: 'Brief reason for opening this ticket',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'close',
|
||||||
|
type: 1,
|
||||||
|
description: 'Close the current ticket',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'admin',
|
||||||
|
description: 'Admin monitoring commands',
|
||||||
|
default_member_permissions: '8',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
type: 1,
|
||||||
|
description: 'View bot status and statistics',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heat',
|
||||||
|
type: 1,
|
||||||
|
description: 'Check heat level of a user',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'user',
|
||||||
|
type: 6,
|
||||||
|
description: 'User to check',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'servers',
|
||||||
|
type: 1,
|
||||||
|
description: 'View all servers the bot is in',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'threats',
|
||||||
|
type: 1,
|
||||||
|
description: 'View current heat map (active threats)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'federation',
|
||||||
|
type: 1,
|
||||||
|
description: 'View federation role mappings',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'federation',
|
||||||
|
description: 'Manage cross-server role sync',
|
||||||
|
default_member_permissions: '8',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'link',
|
||||||
|
type: 1,
|
||||||
|
description: 'Link a role for cross-server syncing',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'role',
|
||||||
|
type: 8,
|
||||||
|
description: 'Role to sync across realms',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'unlink',
|
||||||
|
type: 1,
|
||||||
|
description: 'Remove a role from cross-server syncing',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'role',
|
||||||
|
type: 8,
|
||||||
|
description: 'Role to remove from sync',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'list',
|
||||||
|
type: 1,
|
||||||
|
description: 'List all linked roles',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
description: 'View network status and bot health',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const token = process.env.DISCORD_BOT_TOKEN || process.env.DISCORD_TOKEN;
|
||||||
|
const rest = new REST({ version: '10' }).setToken(token);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
console.log('Registering slash commands...');
|
||||||
|
|
||||||
|
const data = await rest.put(
|
||||||
|
Routes.applicationCommands(process.env.DISCORD_CLIENT_ID),
|
||||||
|
{ body: commands }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Successfully registered ${data.length} commands`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error registering commands:', error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
Loading…
Reference in a new issue