Add Aethex Sentinel bot with security and federation features

Initializes the Aethex Sentinel bot project, including package.json, Prisma schema, core client, configuration, health server, and event listeners for audit logs and member updates. Implements commands for federation management, sentinel security status, and ticket system.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e72fc1b7-94bd-4d6c-801f-cbac2fae245c
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: fc082e4d-cb52-4049-9c2e-69ba6c2e78d4
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/e72fc1b7-94bd-4d6c-801f-cbac2fae245c/jW8PJKQ
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-07 21:10:57 +00:00
parent 6a79a12c06
commit fbd203dcfc
22 changed files with 2438 additions and 1 deletions

View file

@ -1,5 +1,5 @@
entrypoint = "main.py"
modules = ["python-3.10", "postgresql-16"]
modules = ["python-3.10", "postgresql-16", "nodejs-20"]
hidden = [".pythonlibs"]

21
sentinel-bot/.env.example Normal file
View file

@ -0,0 +1,21 @@
# Discord Bot Configuration
DISCORD_TOKEN=your_bot_token_here
# Federation Guild IDs
HUB_ID=your_hub_server_id
FORGE_ID=your_gameforge_server_id
FOUNDATION_ID=your_foundation_server_id
LABS_ID=your_labs_server_id
CORP_ID=your_corp_server_id
# Security Configuration
WHITELISTED_USERS=user_id_1,user_id_2
# Dashboard Configuration
STATUS_CHANNEL_ID=voice_channel_id_for_status
# Health Check
HEALTH_PORT=8044
# Database (auto-provided by Replit)
# DATABASE_URL=postgresql://...

1097
sentinel-bot/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
sentinel-bot/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "aethex-sentinel",
"version": "1.0.0",
"description": "Enterprise-grade Discord bot for managing a federation of servers",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:studio": "prisma studio"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"@sapphire/framework": "^5.2.1",
"@sapphire/plugin-scheduled-tasks": "^10.0.1",
"discord.js": "^14.16.3",
"dotenv": "^16.4.5"
},
"devDependencies": {
"@types/node": "^20.17.6",
"prisma": "^5.22.0",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}
}

View file

@ -0,0 +1,65 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id
heatLevel Int @default(0)
balance Float @default(0.0)
roles String[]
lastHeatReset DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tickets Ticket[]
heatEvents HeatEvent[]
}
model HeatEvent {
id Int @id @default(autoincrement())
userId String
action String
timestamp DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@index([userId, timestamp])
}
model Ticket {
id Int @id @default(autoincrement())
threadId String @unique
userId String
type String
status String @default("open")
transcript String?
createdAt DateTime @default(now())
closedAt DateTime?
user User @relation(fields: [userId], references: [id])
@@index([userId])
}
model GuildConfig {
id String @id
name String
type String
statusChannelId String?
feedChannelId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model RoleMapping {
id Int @id @default(autoincrement())
sourceGuild String
sourceRole String
targetGuild String
targetRole String
roleName String
@@unique([sourceGuild, sourceRole, targetGuild])
}

View file

@ -0,0 +1,102 @@
import { Command } from '@sapphire/framework';
import { PermissionFlagsBits } from 'discord.js';
import { federationManager } from '../modules/federation/FederationManager';
import { prisma } from '../core/client';
export class FederationCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'federation',
description: 'Manage federation role mappings',
requiredUserPermissions: [PermissionFlagsBits.Administrator],
});
}
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addSubcommand((sub) =>
sub
.setName('add-mapping')
.setDescription('Add a role mapping between guilds')
.addStringOption((opt) =>
opt.setName('source-guild').setDescription('Source guild ID').setRequired(true)
)
.addStringOption((opt) =>
opt.setName('source-role').setDescription('Source role ID').setRequired(true)
)
.addStringOption((opt) =>
opt.setName('target-guild').setDescription('Target guild ID').setRequired(true)
)
.addStringOption((opt) =>
opt.setName('target-role').setDescription('Target role ID').setRequired(true)
)
.addStringOption((opt) =>
opt.setName('role-name').setDescription('Friendly name for this mapping').setRequired(true)
)
)
.addSubcommand((sub) =>
sub.setName('list').setDescription('List all role mappings')
)
.addSubcommand((sub) =>
sub.setName('reload').setDescription('Reload mappings from database')
)
);
}
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'add-mapping') {
const sourceGuild = interaction.options.getString('source-guild', true);
const sourceRole = interaction.options.getString('source-role', true);
const targetGuild = interaction.options.getString('target-guild', true);
const targetRole = interaction.options.getString('target-role', true);
const roleName = interaction.options.getString('role-name', true);
await federationManager.addMapping({
sourceGuild,
sourceRole,
targetGuild,
targetRole,
roleName,
});
return interaction.reply({
content: `✅ Added mapping: "${roleName}" from ${sourceGuild} to ${targetGuild}`,
ephemeral: true,
});
}
if (subcommand === 'list') {
const mappings = await prisma.roleMapping.findMany();
if (mappings.length === 0) {
return interaction.reply({
content: 'No role mappings configured.',
ephemeral: true,
});
}
const list = mappings
.map((m) => `• **${m.roleName}**: ${m.sourceGuild}:${m.sourceRole}${m.targetGuild}:${m.targetRole}`)
.join('\n');
return interaction.reply({
content: `**Federation Role Mappings:**\n${list}`,
ephemeral: true,
});
}
if (subcommand === 'reload') {
await federationManager.loadMappings();
return interaction.reply({
content: '✅ Reloaded role mappings from database.',
ephemeral: true,
});
}
}
}

View file

@ -0,0 +1,84 @@
import { Command } from '@sapphire/framework';
import { PermissionFlagsBits, EmbedBuilder } from 'discord.js';
import { heatSystem } from '../modules/security/HeatSystem';
import { config } from '../core/config';
export class SentinelCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'sentinel',
description: 'Manage the Sentinel security system',
requiredUserPermissions: [PermissionFlagsBits.Administrator],
});
}
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addSubcommand((sub) =>
sub.setName('status').setDescription('View Sentinel security status')
)
.addSubcommand((sub) =>
sub
.setName('clear-heat')
.setDescription('Clear heat level for a user')
.addUserOption((opt) =>
opt.setName('user').setDescription('User to clear heat for').setRequired(true)
)
)
.addSubcommand((sub) =>
sub.setName('unlock').setDescription('Lift lockdown mode on this server')
)
);
}
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'status') {
const embed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle('🛡️ Sentinel Security Status')
.addFields(
{ name: 'Heat Threshold', value: `${config.security.heatThreshold} actions`, inline: true },
{ name: 'Window', value: `${config.security.heatWindowMs}ms`, inline: true },
{ name: 'Whitelisted Users', value: `${config.security.whitelistedUsers.length}`, inline: true },
{
name: 'Monitored Actions',
value: config.security.dangerousActions.join(', '),
inline: false,
}
)
.setTimestamp();
return interaction.reply({ embeds: [embed], ephemeral: true });
}
if (subcommand === 'clear-heat') {
const user = interaction.options.getUser('user', true);
heatSystem.clearHeat(user.id);
return interaction.reply({
content: `✅ Cleared heat for ${user.tag}`,
ephemeral: true,
});
}
if (subcommand === 'unlock') {
if (!interaction.guild) {
return interaction.reply({
content: 'This command must be used in a server.',
ephemeral: true,
});
}
await heatSystem.unlockGuild(interaction.guild);
return interaction.reply({
content: '🔓 Lockdown lifted. @everyone permissions restored.',
ephemeral: true,
});
}
}
}

View file

@ -0,0 +1,55 @@
import { Command } from '@sapphire/framework';
import { EmbedBuilder } from 'discord.js';
import { config } from '../core/config';
export class StatusCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'status',
description: 'View Sentinel bot and network status',
});
}
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder.setName(this.name).setDescription(this.description)
);
}
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const client = this.container.client;
const guildIds = Object.values(config.guilds).filter(id => id);
const guilds = guildIds
.map(id => client.guilds.cache.get(id))
.filter(g => g !== undefined);
const totalMembers = guilds.reduce((sum, g) => sum + g!.memberCount, 0);
const guildList = guilds
.map(g => `• **${g!.name}**: ${g!.memberCount.toLocaleString()} members`)
.join('\n') || 'No federation guilds connected';
const uptimeSeconds = Math.floor(process.uptime());
const hours = Math.floor(uptimeSeconds / 3600);
const minutes = Math.floor((uptimeSeconds % 3600) / 60);
const seconds = uptimeSeconds % 60;
const uptimeStr = `${hours}h ${minutes}m ${seconds}s`;
const embed = new EmbedBuilder()
.setColor(0x5865f2)
.setTitle('🛡️ Aethex Sentinel Status')
.setThumbnail(client.user?.displayAvatarURL() || null)
.addFields(
{ name: '📡 Network', value: `${guilds.length} guilds`, inline: true },
{ name: '👥 Total Members', value: totalMembers.toLocaleString(), inline: true },
{ name: '⏱️ Uptime', value: uptimeStr, inline: true },
{ name: '🏰 Federation Guilds', value: guildList, inline: false }
)
.setFooter({ text: 'Aethex Sentinel • Protecting the Federation' })
.setTimestamp();
return interaction.reply({ embeds: [embed] });
}
}

View file

@ -0,0 +1,76 @@
import { Command } from '@sapphire/framework';
import { PermissionFlagsBits, ChannelType, TextChannel } from 'discord.js';
import { ticketManager } from '../modules/commerce/TicketManager';
import { prisma } from '../core/client';
export class TicketCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'ticket',
description: 'Manage the ticket system',
requiredUserPermissions: [PermissionFlagsBits.Administrator],
});
}
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addSubcommand((sub) =>
sub
.setName('setup')
.setDescription('Set up the ticket panel in this channel')
)
.addSubcommand((sub) =>
sub.setName('stats').setDescription('View ticket statistics')
)
);
}
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'setup') {
if (!interaction.channel || interaction.channel.type !== ChannelType.GuildText) {
return interaction.reply({
content: 'This command must be used in a text channel.',
ephemeral: true,
});
}
await interaction.deferReply({ ephemeral: true });
await ticketManager.createSupportPanel(interaction.channel as TextChannel);
return interaction.editReply({
content: '✅ Ticket panel created!',
});
}
if (subcommand === 'stats') {
const totalTickets = await prisma.ticket.count();
const openTickets = await prisma.ticket.count({ where: { status: 'open' } });
const closedTickets = await prisma.ticket.count({ where: { status: 'closed' } });
const byType = await prisma.ticket.groupBy({
by: ['type'],
_count: { id: true },
});
const typeStats = byType
.map((t) => `${t.type}: ${t._count.id}`)
.join('\n') || 'No tickets yet';
return interaction.reply({
content:
`**🎫 Ticket Statistics**\n\n` +
`Total: ${totalTickets}\n` +
`Open: ${openTickets}\n` +
`Closed: ${closedTickets}\n\n` +
`**By Type:**\n${typeStats}`,
ephemeral: true,
});
}
}
}

View file

@ -0,0 +1,37 @@
import { SapphireClient } from '@sapphire/framework';
import { GatewayIntentBits, Partials } from 'discord.js';
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
export class SentinelClient extends SapphireClient {
public constructor() {
super({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildModeration,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages,
],
partials: [
Partials.Channel,
Partials.Message,
Partials.GuildMember,
],
loadMessageCommandListeners: true,
});
}
public override async login(token?: string): Promise<string> {
await prisma.$connect();
console.log('✅ Database connected');
return super.login(token);
}
public override async destroy(): Promise<void> {
await prisma.$disconnect();
return super.destroy();
}
}

View file

@ -0,0 +1,41 @@
import 'dotenv/config';
export const config = {
token: process.env.DISCORD_TOKEN!,
guilds: {
hub: process.env.HUB_ID!,
gameforge: process.env.FORGE_ID!,
foundation: process.env.FOUNDATION_ID!,
labs: process.env.LABS_ID!,
corp: process.env.CORP_ID!,
},
security: {
heatThreshold: 3,
heatWindowMs: 5000,
dangerousActions: ['CHANNEL_DELETE', 'BAN_ADD', 'KICK', 'ROLE_DELETE', 'WEBHOOK_DELETE'],
whitelistedUsers: process.env.WHITELISTED_USERS?.split(',') || [],
},
dashboard: {
updateIntervalMs: 5 * 60 * 1000,
statusChannelId: process.env.STATUS_CHANNEL_ID,
},
health: {
port: parseInt(process.env.HEALTH_PORT || '8044'),
},
};
export function validateConfig(): void {
const required = ['DISCORD_TOKEN'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error('❌ Missing required environment variables:', missing.join(', '));
process.exit(1);
}
console.log('✅ Configuration validated');
}

View file

@ -0,0 +1,59 @@
import http from 'http';
import { SentinelClient } from './client';
import { config } from './config';
export function startHealthServer(client: SentinelClient): void {
const server = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Content-Type', 'application/json');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
if (req.url === '/health') {
const guilds = client.guilds.cache.size;
const commands = client.stores.get('commands')?.size || 0;
res.writeHead(200);
res.end(JSON.stringify({
status: client.isReady() ? 'online' : 'offline',
guilds,
commands,
uptime: Math.floor(process.uptime()),
timestamp: new Date().toISOString(),
bot: {
tag: client.user?.tag || 'Not ready',
id: client.user?.id,
},
}));
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()),
}));
return;
}
res.writeHead(404);
res.end(JSON.stringify({ error: 'Not found' }));
});
server.listen(config.health.port, () => {
console.log(`🏥 Health server running on port ${config.health.port}`);
});
}

27
sentinel-bot/src/index.ts Normal file
View file

@ -0,0 +1,27 @@
import { config, validateConfig } from './core/config';
import { SentinelClient } from './core/client';
import { startHealthServer } from './core/health';
validateConfig();
const client = new SentinelClient();
client.once('ready', () => {
console.log(`✅ Aethex Sentinel logged in as ${client.user?.tag}`);
console.log(`📡 Watching ${client.guilds.cache.size} guild(s)`);
client.user?.setActivity('🛡️ Protecting the Federation', { type: 3 });
startHealthServer(client);
});
process.on('unhandledRejection', (error: Error) => {
console.error('❌ Unhandled Promise Rejection:', error);
});
process.on('uncaughtException', (error: Error) => {
console.error('❌ Uncaught Exception:', error);
process.exit(1);
});
client.login(config.token);

View file

@ -0,0 +1,40 @@
import { Listener } from '@sapphire/framework';
import { AuditLogEvent, GuildAuditLogsEntry, Guild } from 'discord.js';
import { heatSystem } from '../modules/security/HeatSystem';
type DangerousAction = 'CHANNEL_DELETE' | 'BAN_ADD' | 'KICK' | 'ROLE_DELETE' | 'WEBHOOK_DELETE';
const DANGEROUS_ACTIONS: Record<number, DangerousAction> = {
[AuditLogEvent.ChannelDelete]: 'CHANNEL_DELETE',
[AuditLogEvent.MemberBanAdd]: 'BAN_ADD',
[AuditLogEvent.MemberKick]: 'KICK',
[AuditLogEvent.RoleDelete]: 'ROLE_DELETE',
[AuditLogEvent.WebhookDelete]: 'WEBHOOK_DELETE',
};
export class AuditLogCreateListener extends Listener {
public constructor(context: Listener.LoaderContext, options: Listener.Options) {
super(context, {
...options,
event: 'guildAuditLogEntryCreate',
});
}
public async run(entry: GuildAuditLogsEntry, guild: Guild): Promise<void> {
const actionType = DANGEROUS_ACTIONS[entry.action];
if (!actionType) return;
const executorId = entry.executorId;
if (!executorId) return;
if (executorId === this.container.client.user?.id) return;
console.log(`⚠️ Dangerous action detected: ${actionType} by ${executorId} in ${guild.name}`);
const triggered = await heatSystem.recordAction(executorId, actionType, guild);
if (triggered) {
console.log(`🚨 Anti-nuke triggered for ${executorId} in ${guild.name}`);
}
}
}

View file

@ -0,0 +1,28 @@
import { Listener } from '@sapphire/framework';
import { GuildMember, Role } from 'discord.js';
import { federationManager } from '../modules/federation/FederationManager';
export class GuildMemberUpdateListener extends Listener {
public constructor(context: Listener.LoaderContext, options: Listener.Options) {
super(context, {
...options,
event: 'guildMemberUpdate',
});
}
public async run(oldMember: GuildMember, newMember: GuildMember): Promise<void> {
const oldRoles = oldMember.roles.cache;
const newRoles = newMember.roles.cache;
const addedRoles = newRoles.filter(role => !oldRoles.has(role.id));
const removedRoles = oldRoles.filter(role => !newRoles.has(role.id));
for (const role of addedRoles.values()) {
await federationManager.syncRoleGrant(newMember, role);
}
for (const role of removedRoles.values()) {
await federationManager.syncRoleRemove(newMember, role);
}
}
}

View file

@ -0,0 +1,46 @@
import { Listener } from '@sapphire/framework';
import { Interaction } from 'discord.js';
import { ticketManager } from '../modules/commerce/TicketManager';
export class InteractionCreateListener extends Listener {
public constructor(context: Listener.LoaderContext, options: Listener.Options) {
super(context, {
...options,
event: 'interactionCreate',
});
}
public async run(interaction: Interaction): Promise<void> {
if (!interaction.isButton()) return;
const customId = interaction.customId;
try {
if (customId === 'ticket_rental') {
await ticketManager.createTicket(interaction, 'rental');
} else if (customId === 'ticket_support') {
await ticketManager.createTicket(interaction, 'support');
} else if (customId === 'ticket_billing') {
await ticketManager.createTicket(interaction, 'billing');
} else if (customId === 'ticket_invoice') {
await ticketManager.generateInvoice(interaction);
} else if (customId === 'ticket_close') {
await ticketManager.closeTicket(interaction);
} else if (customId === 'invoice_pay') {
await interaction.reply({
content: '✅ Invoice marked as paid! (Mock transaction)',
ephemeral: true,
});
}
} catch (error) {
console.error('❌ Button interaction error:', error);
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: 'An error occurred while processing your request.',
ephemeral: true,
});
}
}
}
}

View file

@ -0,0 +1,28 @@
import { Listener } from '@sapphire/framework';
import { Client } from 'discord.js';
import { federationManager } from '../modules/federation/FederationManager';
import { StatusUpdater } from '../modules/dashboard/StatusUpdater';
export class ReadyListener extends Listener {
public constructor(context: Listener.LoaderContext, options: Listener.Options) {
super(context, {
...options,
once: true,
event: 'ready',
});
}
public async run(client: Client<true>): Promise<void> {
console.log(`✅ Aethex Sentinel ready as ${client.user.tag}`);
console.log(`📡 Connected to ${client.guilds.cache.size} guild(s)`);
await federationManager.loadMappings();
const statusUpdater = new StatusUpdater(client);
statusUpdater.start();
client.user.setActivity('🛡️ Protecting the Federation', { type: 3 });
console.log('✅ All modules initialized');
}
}

View file

@ -0,0 +1,202 @@
import {
ButtonInteraction,
ChannelType,
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
TextChannel,
ThreadChannel,
Message,
} from 'discord.js';
import { prisma } from '../../core/client';
export class TicketManager {
async createTicket(interaction: ButtonInteraction, type: string): Promise<void> {
const channel = interaction.channel as TextChannel;
if (!channel || channel.type !== ChannelType.GuildText) {
await interaction.reply({ content: 'Cannot create ticket here.', ephemeral: true });
return;
}
await prisma.user.upsert({
where: { id: interaction.user.id },
update: {},
create: { id: interaction.user.id },
});
const thread = await channel.threads.create({
name: `${type}-${interaction.user.username}-${Date.now().toString(36)}`,
type: ChannelType.PrivateThread,
invitable: false,
reason: `Ticket created by ${interaction.user.tag}`,
});
await thread.members.add(interaction.user.id);
await prisma.ticket.create({
data: {
threadId: thread.id,
userId: interaction.user.id,
type,
status: 'open',
},
});
const embed = new EmbedBuilder()
.setColor(0x5865f2)
.setTitle(`🎫 ${type.charAt(0).toUpperCase() + type.slice(1)} Ticket`)
.setDescription(
`Welcome ${interaction.user}!\n\n` +
`A staff member will be with you shortly.\n` +
`Please describe your request in detail.`
)
.setFooter({ text: `Ticket ID: ${thread.id}` })
.setTimestamp();
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('ticket_invoice')
.setLabel('Generate Invoice')
.setStyle(ButtonStyle.Primary)
.setEmoji('📄'),
new ButtonBuilder()
.setCustomId('ticket_close')
.setLabel('Close Ticket')
.setStyle(ButtonStyle.Danger)
.setEmoji('🔒')
);
await thread.send({ embeds: [embed], components: [row] });
await interaction.reply({
content: `✅ Your ticket has been created: ${thread}`,
ephemeral: true,
});
console.log(`🎫 Ticket created: ${thread.name} by ${interaction.user.tag}`);
}
async generateInvoice(interaction: ButtonInteraction): Promise<void> {
const invoiceId = `INV-${Date.now().toString(36).toUpperCase()}`;
const embed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle('📄 Invoice Generated')
.setDescription('Here is your mock invoice for the requested service.')
.addFields(
{ name: 'Invoice ID', value: invoiceId, inline: true },
{ name: 'Status', value: 'Pending', inline: true },
{ name: 'Amount', value: '$XX.XX', inline: true },
{ name: 'Service', value: 'Server Rental', inline: false },
)
.setFooter({ text: 'This is a mock invoice for demonstration' })
.setTimestamp();
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('invoice_pay')
.setLabel('Mark as Paid')
.setStyle(ButtonStyle.Success)
.setEmoji('💳')
);
await interaction.reply({ embeds: [embed], components: [row] });
}
async closeTicket(interaction: ButtonInteraction): Promise<void> {
const thread = interaction.channel as ThreadChannel;
if (!thread || thread.type !== ChannelType.PrivateThread) {
await interaction.reply({ content: 'This is not a ticket thread.', ephemeral: true });
return;
}
const ticket = await prisma.ticket.findUnique({
where: { threadId: thread.id },
});
if (!ticket) {
await interaction.reply({ content: 'Ticket not found in database.', ephemeral: true });
return;
}
await interaction.reply({ content: '🔒 Closing ticket and saving transcript...' });
const transcript = await this.generateTranscript(thread);
await prisma.ticket.update({
where: { threadId: thread.id },
data: {
status: 'closed',
transcript,
closedAt: new Date(),
},
});
await thread.setLocked(true, 'Ticket closed');
await thread.setArchived(true, 'Ticket closed');
console.log(`🎫 Ticket closed: ${thread.name}`);
}
private async generateTranscript(thread: ThreadChannel): Promise<string> {
const messages: Message[] = [];
let lastId: string | undefined;
while (true) {
const batch = await thread.messages.fetch({
limit: 100,
before: lastId,
});
if (batch.size === 0) break;
messages.push(...batch.values());
lastId = batch.last()?.id;
}
messages.reverse();
const transcript = messages
.map(m => `[${m.createdAt.toISOString()}] ${m.author.tag}: ${m.content}`)
.join('\n');
return transcript;
}
async createSupportPanel(channel: TextChannel): Promise<void> {
const embed = new EmbedBuilder()
.setColor(0x5865f2)
.setTitle('🎫 Support Center')
.setDescription(
'Need help? Click a button below to open a support ticket.\n\n' +
'**Available Services:**\n' +
'• Server Rental - Rent a game server\n' +
'• Technical Support - Get help with issues\n' +
'• Billing - Payment and invoice questions'
)
.setFooter({ text: 'Aethex Sentinel Support System' });
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('ticket_rental')
.setLabel('Rent Server')
.setStyle(ButtonStyle.Primary)
.setEmoji('🖥️'),
new ButtonBuilder()
.setCustomId('ticket_support')
.setLabel('Technical Support')
.setStyle(ButtonStyle.Secondary)
.setEmoji('🔧'),
new ButtonBuilder()
.setCustomId('ticket_billing')
.setLabel('Billing')
.setStyle(ButtonStyle.Secondary)
.setEmoji('💰')
);
await channel.send({ embeds: [embed], components: [row] });
}
}
export const ticketManager = new TicketManager();

View file

@ -0,0 +1,126 @@
import { Client, VoiceChannel, ChannelType } from 'discord.js';
import { prisma } from '../../core/client';
import { config } from '../../core/config';
export class StatusUpdater {
private client: Client;
private intervalId: NodeJS.Timeout | null = null;
constructor(client: Client) {
this.client = client;
}
start(): void {
if (this.intervalId) return;
this.update();
this.intervalId = setInterval(() => {
this.update();
}, config.dashboard.updateIntervalMs);
console.log(`📊 Status updater started (interval: ${config.dashboard.updateIntervalMs}ms)`);
}
stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
console.log('📊 Status updater stopped');
}
}
async update(): Promise<void> {
try {
const guildIds = Object.values(config.guilds).filter(id => id);
let totalMembers = 0;
const guildStats: { id: string; name: string; members: number }[] = [];
for (const guildId of guildIds) {
const guild = this.client.guilds.cache.get(guildId);
if (guild) {
totalMembers += guild.memberCount;
guildStats.push({
id: guild.id,
name: guild.name,
members: guild.memberCount,
});
await prisma.guildConfig.upsert({
where: { id: guild.id },
update: {
name: guild.name,
updatedAt: new Date(),
},
create: {
id: guild.id,
name: guild.name,
type: this.getGuildType(guild.id),
},
});
}
}
if (config.dashboard.statusChannelId && config.guilds.hub) {
await this.updateStatusChannel(totalMembers);
}
console.log(`📊 Network status: ${totalMembers} total members across ${guildStats.length} guilds`);
} catch (error) {
console.error('❌ Failed to update status:', error);
}
}
private async updateStatusChannel(totalMembers: number): Promise<void> {
try {
const hubGuild = this.client.guilds.cache.get(config.guilds.hub);
if (!hubGuild) return;
const channel = hubGuild.channels.cache.get(config.dashboard.statusChannelId!);
if (!channel || channel.type !== ChannelType.GuildVoice) return;
const voiceChannel = channel as VoiceChannel;
const newName = `🟢 Network: ${totalMembers.toLocaleString()} Users`;
if (voiceChannel.name !== newName) {
await voiceChannel.setName(newName);
console.log(`📊 Updated status channel: ${newName}`);
}
} catch (error) {
console.error('❌ Failed to update status channel:', error);
}
}
private getGuildType(guildId: string): string {
const entries = Object.entries(config.guilds);
const found = entries.find(([, id]) => id === guildId);
return found ? found[0] : 'unknown';
}
async getNetworkStats(): Promise<{
totalMembers: number;
totalGuilds: number;
guilds: { id: string; name: string; members: number; type: string }[];
}> {
const guildIds = Object.values(config.guilds).filter(id => id);
const guilds: { id: string; name: string; members: number; type: string }[] = [];
for (const guildId of guildIds) {
const guild = this.client.guilds.cache.get(guildId);
if (guild) {
guilds.push({
id: guild.id,
name: guild.name,
members: guild.memberCount,
type: this.getGuildType(guild.id),
});
}
}
return {
totalMembers: guilds.reduce((sum, g) => sum + g.members, 0),
totalGuilds: guilds.length,
guilds,
};
}
}

View file

@ -0,0 +1,124 @@
import { Guild, GuildMember, Role } from 'discord.js';
import { prisma } from '../../core/client';
import { config } from '../../core/config';
interface RoleMapping {
sourceGuild: string;
sourceRole: string;
targetGuild: string;
targetRole: string;
roleName: string;
}
export class FederationManager {
private roleMappings: RoleMapping[] = [];
async loadMappings(): Promise<void> {
this.roleMappings = await prisma.roleMapping.findMany();
console.log(`📋 Loaded ${this.roleMappings.length} role mappings`);
}
async addMapping(mapping: Omit<RoleMapping, 'id'>): Promise<void> {
await prisma.roleMapping.create({
data: mapping,
});
await this.loadMappings();
}
async syncRoleGrant(member: GuildMember, role: Role): Promise<void> {
const sourceGuildId = member.guild.id;
const mappings = this.roleMappings.filter(
m => m.sourceGuild === sourceGuildId && m.sourceRole === role.id
);
if (mappings.length === 0) return;
console.log(`🔄 Syncing role "${role.name}" for ${member.user.tag} to ${mappings.length} guild(s)`);
for (const mapping of mappings) {
try {
const targetGuild = member.client.guilds.cache.get(mapping.targetGuild);
if (!targetGuild) {
console.warn(`⚠️ Target guild ${mapping.targetGuild} not found`);
continue;
}
const targetMember = await targetGuild.members.fetch(member.id).catch(() => null);
if (!targetMember) {
console.warn(`⚠️ User ${member.user.tag} not in target guild ${targetGuild.name}`);
continue;
}
const targetRole = targetGuild.roles.cache.get(mapping.targetRole);
if (!targetRole) {
console.warn(`⚠️ Target role ${mapping.targetRole} not found in ${targetGuild.name}`);
continue;
}
if (!targetMember.roles.cache.has(targetRole.id)) {
await targetMember.roles.add(targetRole, `Federation sync from ${member.guild.name}`);
console.log(`✅ Granted "${targetRole.name}" to ${member.user.tag} in ${targetGuild.name}`);
}
} catch (error) {
console.error(`❌ Failed to sync role to guild ${mapping.targetGuild}:`, error);
}
}
await this.updateUserRoles(member.id);
}
async syncRoleRemove(member: GuildMember, role: Role): Promise<void> {
const sourceGuildId = member.guild.id;
const mappings = this.roleMappings.filter(
m => m.sourceGuild === sourceGuildId && m.sourceRole === role.id
);
if (mappings.length === 0) return;
console.log(`🔄 Removing synced role "${role.name}" for ${member.user.tag} from ${mappings.length} guild(s)`);
for (const mapping of mappings) {
try {
const targetGuild = member.client.guilds.cache.get(mapping.targetGuild);
if (!targetGuild) continue;
const targetMember = await targetGuild.members.fetch(member.id).catch(() => null);
if (!targetMember) continue;
const targetRole = targetGuild.roles.cache.get(mapping.targetRole);
if (!targetRole) continue;
if (targetMember.roles.cache.has(targetRole.id)) {
await targetMember.roles.remove(targetRole, `Federation sync removal from ${member.guild.name}`);
console.log(`✅ Removed "${targetRole.name}" from ${member.user.tag} in ${targetGuild.name}`);
}
} catch (error) {
console.error(`❌ Failed to remove synced role:`, error);
}
}
await this.updateUserRoles(member.id);
}
private async updateUserRoles(userId: string): Promise<void> {
const syncedRoles = this.roleMappings
.filter(m => m.sourceGuild === config.guilds.hub)
.map(m => m.roleName);
await prisma.user.upsert({
where: { id: userId },
update: { roles: syncedRoles },
create: { id: userId, roles: syncedRoles },
});
}
getGuildType(guildId: string): string | null {
const entries = Object.entries(config.guilds);
const found = entries.find(([, id]) => id === guildId);
return found ? found[0] : null;
}
}
export const federationManager = new FederationManager();

View file

@ -0,0 +1,131 @@
import { Guild, GuildMember, PermissionFlagsBits } from 'discord.js';
import { prisma } from '../../core/client';
import { config } from '../../core/config';
type DangerousAction = 'CHANNEL_DELETE' | 'BAN_ADD' | 'KICK' | 'ROLE_DELETE' | 'WEBHOOK_DELETE';
interface HeatEvent {
userId: string;
action: DangerousAction;
timestamp: Date;
}
export class HeatSystem {
private heatCache: Map<string, HeatEvent[]> = new Map();
isWhitelisted(userId: string): boolean {
return config.security.whitelistedUsers.includes(userId);
}
async recordAction(userId: string, action: DangerousAction, guild: Guild): Promise<boolean> {
if (this.isWhitelisted(userId)) {
return false;
}
const now = new Date();
const event: HeatEvent = { userId, action, timestamp: now };
await prisma.heatEvent.create({
data: { userId, action, timestamp: now },
});
const events = this.heatCache.get(userId) || [];
events.push(event);
const windowStart = new Date(now.getTime() - config.security.heatWindowMs);
const recentEvents = events.filter(e => e.timestamp >= windowStart);
this.heatCache.set(userId, recentEvents);
console.log(`🔥 Heat event: ${action} by ${userId} (${recentEvents.length}/${config.security.heatThreshold})`);
if (recentEvents.length >= config.security.heatThreshold) {
console.log(`🚨 THRESHOLD EXCEEDED for ${userId}!`);
await this.triggerLockdown(userId, guild, recentEvents);
return true;
}
return false;
}
private async triggerLockdown(userId: string, guild: Guild, events: HeatEvent[]): Promise<void> {
console.log(`🔒 LOCKDOWN INITIATED in ${guild.name}`);
try {
const member = await guild.members.fetch(userId).catch(() => null);
if (member && member.bannable) {
await member.ban({
reason: `[SENTINEL] Anti-nuke triggered: ${events.length} dangerous actions in ${config.security.heatWindowMs}ms`,
deleteMessageSeconds: 0,
});
console.log(`🔨 Banned ${member.user.tag} for nuke attempt`);
}
} catch (error) {
console.error('❌ Failed to ban attacker:', error);
}
try {
const everyoneRole = guild.roles.everyone;
await everyoneRole.setPermissions(
everyoneRole.permissions.remove([
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.AddReactions,
PermissionFlagsBits.CreatePublicThreads,
PermissionFlagsBits.CreatePrivateThreads,
]),
'[SENTINEL] Lockdown mode activated'
);
console.log(`🔒 Locked down @everyone permissions`);
} catch (error) {
console.error('❌ Failed to modify @everyone permissions:', error);
}
await this.sendLockdownAlert(guild, userId, events);
await prisma.user.upsert({
where: { id: userId },
update: { heatLevel: events.length },
create: { id: userId, heatLevel: events.length },
});
}
private async sendLockdownAlert(guild: Guild, attackerId: string, events: HeatEvent[]): Promise<void> {
const owner = await guild.fetchOwner().catch(() => null);
if (!owner) return;
try {
await owner.send({
content: `🚨 **LOCKDOWN ALERT** 🚨\n\n` +
`Server: **${guild.name}**\n` +
`Attacker ID: \`${attackerId}\`\n` +
`Actions detected: ${events.length}\n` +
`Actions: ${events.map(e => e.action).join(', ')}\n\n` +
`The attacker has been banned and @everyone permissions have been restricted.\n` +
`Please review audit logs and restore permissions when safe.`,
});
} catch (error) {
console.error('❌ Failed to DM server owner:', error);
}
}
async unlockGuild(guild: Guild): Promise<void> {
try {
const everyoneRole = guild.roles.everyone;
await everyoneRole.setPermissions(
everyoneRole.permissions.add([
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.AddReactions,
]),
'[SENTINEL] Lockdown lifted'
);
console.log(`🔓 Lifted lockdown in ${guild.name}`);
} catch (error) {
console.error('❌ Failed to unlock guild:', error);
}
}
clearHeat(userId: string): void {
this.heatCache.delete(userId);
}
}
export const heatSystem = new HeatSystem();

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}