Make Supabase features optional and integrate new security systems

Updates bot.js to make Supabase integration optional, adds Sentinel security listeners, and modifies several commands to handle missing Supabase configurations gracefully. Also updates package.json and replit.md for new dependencies and features.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 0d645005-4840-49ef-9446-2c62d2bb7eed
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/Wmps8l5
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-07 23:41:11 +00:00
parent 3e4ac076e2
commit b178664f99
19 changed files with 1285 additions and 199 deletions

View file

@ -22,6 +22,10 @@ externalPort = 80
localPort = 8080 localPort = 8080
externalPort = 8080 externalPort = 8080
[[ports]]
localPort = 38431
externalPort = 3000
[workflows] [workflows]
runButton = "Project" runButton = "Project"

View file

@ -1,21 +1,48 @@
# Required # Discord Bot Configuration
DISCORD_BOT_TOKEN=your_discord_bot_token DISCORD_BOT_TOKEN=your_bot_token_here
DISCORD_CLIENT_ID=your_discord_client_id DISCORD_CLIENT_ID=your_client_id_here
DISCORD_PUBLIC_KEY=your_public_key_here
# Optional - Supabase (for user verification features) # Supabase Configuration (optional - community features require this)
SUPABASE_URL=your_supabase_url SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE=your_supabase_service_role_key SUPABASE_SERVICE_ROLE=your_service_role_key_here
# Optional - Federation Guild IDs # API Configuration
VITE_API_BASE=https://api.aethex.dev
# Discord Feed Webhook Configuration
DISCORD_FEED_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN
DISCORD_FEED_GUILD_ID=515711457946632232
DISCORD_FEED_CHANNEL_ID=1425114041021497454
# Discord Main Chat Channels (comma-separated channel IDs for feed sync)
DISCORD_MAIN_CHAT_CHANNELS=channel_id_1,channel_id_2
# Discord Announcement Channels (comma-separated channel IDs)
DISCORD_ANNOUNCEMENT_CHANNELS=1435667453244866702,your_other_channel_ids_here
# Discord Role Mappings (optional)
DISCORD_FOUNDER_ROLE_ID=your_role_id_here
DISCORD_ADMIN_ROLE_ID=your_admin_role_id_here
# Admin API Tokens
DISCORD_ADMIN_TOKEN=aethex-bot-admin
DISCORD_BRIDGE_TOKEN=aethex-bridge
# Health Server
HEALTH_PORT=8080
# =============================================================================
# SENTINEL SECURITY CONFIGURATION
# =============================================================================
# Federation Guild IDs (optional)
HUB_GUILD_ID=main_hub_server_id HUB_GUILD_ID=main_hub_server_id
LABS_GUILD_ID=labs_server_id LABS_GUILD_ID=labs_server_id
GAMEFORGE_GUILD_ID=gameforge_server_id GAMEFORGE_GUILD_ID=gameforge_server_id
CORP_GUILD_ID=corp_server_id CORP_GUILD_ID=corp_server_id
FOUNDATION_GUILD_ID=foundation_server_id FOUNDATION_GUILD_ID=foundation_server_id
# Optional - Security # Security Settings
WHITELISTED_USERS=user_id_1,user_id_2 WHITELISTED_USERS=user_id_1,user_id_2
ALERT_CHANNEL_ID=channel_id_for_alerts ALERT_CHANNEL_ID=channel_id_for_alerts
# Optional - Health server
HEALTH_PORT=8080

View file

@ -0,0 +1,211 @@
# AeThex Discord Bot - Spaceship Deployment Guide
## 📋 Prerequisites
- Spaceship hosting account with Node.js support
- Discord bot credentials (already in your environment variables)
- Supabase project credentials
- Git access to your repository
## 🚀 Deployment Steps
### Step 1: Prepare the Bot Directory
Ensure all bot files are committed:
```
code/discord-bot/
├── bot.js
├── package.json
├── .env.example
├── Dockerfile
└── commands/
├── verify.js
├── set-realm.js
├── profile.js
├── unlink.js
└── verify-role.js
```
### Step 2: Create Node.js App on Spaceship
1. Log in to your Spaceship hosting dashboard
2. Click "Create New Application"
3. Select **Node.js** as the runtime
4. Name it: `aethex-discord-bot`
5. Select your repository and branch
### Step 3: Configure Environment Variables
In Spaceship Application Settings → Environment Variables, add:
```
DISCORD_BOT_TOKEN=<your_bot_token_from_discord_developer_portal>
DISCORD_CLIENT_ID=<your_client_id>
DISCORD_PUBLIC_KEY=<your_public_key>
SUPABASE_URL=<your_supabase_url>
SUPABASE_SERVICE_ROLE=<your_service_role_key>
BOT_PORT=3000
NODE_ENV=production
```
**Note:** Get these values from:
- Discord Developer Portal: Applications → Your Bot → Token & General Information
- Supabase Dashboard: Project Settings → API
### Step 4: Configure Build & Run Settings
In Spaceship Application Settings:
**Build Command:**
```bash
npm install
```
**Start Command:**
```bash
npm start
```
**Root Directory:**
```
code/discord-bot
```
### Step 5: Deploy
1. Click "Deploy" in Spaceship dashboard
2. Monitor logs for:
```
✅ Bot logged in as <BOT_NAME>#<ID>
📡 Listening in X server(s)
✅ Successfully registered X slash commands.
```
### Step 6: Verify Bot is Online
Once deployed:
1. Go to your Discord server
2. Type `/verify` - the command autocomplete should appear
3. Bot should be online with status "Listening to /verify to link your AeThex account"
## 📡 Discord Bot Endpoints
The bot will be accessible at:
```
https://<your-spaceship-domain>/
```
The bot uses Discord's WebSocket connection (not HTTP), so it doesn't need to expose HTTP endpoints. It listens to Discord events via `client.login(DISCORD_BOT_TOKEN)`.
## 🔌 API Integration
Frontend calls to link Discord accounts:
- **Endpoint:** `POST /api/discord/link`
- **Body:** `{ verification_code, user_id }`
- **Response:** `{ success: true, message: "..." }`
Discord Verify page (`/discord-verify?code=XXX`) will automatically:
1. Call `/api/discord/link` with the verification code
2. Link the Discord ID to the AeThex user account
3. Redirect to dashboard on success
## 🛠️ Debugging
### Check bot logs on Spaceship:
- Application → Logs
- Filter for "bot.js" or "error"
### Common issues:
**"Discord bot not responding to commands"**
- Check: `DISCORD_BOT_TOKEN` is correct
- Check: Bot is added to the Discord server with "applications.commands" scope
- Check: Spaceship logs show "✅ Logged in"
**"Supabase verification fails"**
- Check: `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE` are correct
- Check: `discord_links` and `discord_verifications` tables exist
- Run migration: `code/supabase/migrations/20250107_add_discord_integration.sql`
**"Slash commands not appearing in Discord"**
- Check: Logs show "✅ Successfully registered X slash commands"
- Discord may need 1-2 minutes to sync commands
- Try typing `/` in Discord to force refresh
- Check: Bot has "applications.commands" permission in server
## 📊 Monitoring
### Key metrics to monitor:
- Bot uptime (should be 24/7)
- Command usage (in Supabase)
- Verification code usage (in Supabase)
- Discord role sync success rate
### View in Admin Dashboard:
- AeThex Admin Panel → Discord Management tab
- Shows:
- Bot status
- Servers connected
- Linked accounts count
- Role mapping status
## 🔄 Updating the Bot
1. Make code changes locally
2. Test with `npm start`
3. Commit and push to your branch
4. Spaceship will auto-deploy on push
5. Monitor logs to ensure deployment succeeds
## 🆘 Support
For issues:
1. Check Spaceship logs
2. Review `/api/discord/link` endpoint response
3. Verify all environment variables are set correctly
4. Ensure Supabase tables exist and have correct schema
## 📝 Database Setup
Run this migration on your AeThex Supabase:
```sql
-- From code/supabase/migrations/20250107_add_discord_integration.sql
-- This creates:
-- - discord_links (links Discord ID to AeThex user)
-- - discord_verifications (temporary verification codes)
-- - discord_role_mappings (realm → Discord role mapping)
-- - discord_user_roles (tracking assigned roles)
```
## 🎉 You're All Set!
Once deployed, users can:
1. Click "Link Discord" in their profile settings
2. Type `/verify` in Discord
3. Click the verification link
4. Their Discord account is linked to their AeThex account
5. They can use `/set-realm`, `/profile`, `/unlink`, and `/verify-role` commands
---
**Deployment Date:** `<date>`
**Bot Status:** `<status>`
**Last Updated:** `<timestamp>`

22
aethex-bot/Dockerfile Normal file
View file

@ -0,0 +1,22 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install --production
# Copy bot source
COPY . .
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
# Start bot
CMD ["npm", "start"]

View file

@ -14,6 +14,10 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
require("dotenv").config(); require("dotenv").config();
// =============================================================================
// ENVIRONMENT VALIDATION (Modified: Supabase now optional)
// =============================================================================
const token = process.env.DISCORD_BOT_TOKEN; const token = process.env.DISCORD_BOT_TOKEN;
const clientId = process.env.DISCORD_CLIENT_ID; const clientId = process.env.DISCORD_CLIENT_ID;
@ -29,6 +33,10 @@ if (!clientId) {
console.log("[Token] Bot token loaded (length: " + token.length + " chars)"); console.log("[Token] Bot token loaded (length: " + token.length + " chars)");
// =============================================================================
// DISCORD CLIENT SETUP (Modified: Added intents for Sentinel)
// =============================================================================
const client = new Client({ const client = new Client({
intents: [ intents: [
GatewayIntentBits.Guilds, GatewayIntentBits.Guilds,
@ -40,6 +48,10 @@ const client = new Client({
], ],
}); });
// =============================================================================
// SUPABASE SETUP (Modified: Now optional)
// =============================================================================
let supabase = null; let supabase = null;
if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) { if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) {
supabase = createClient( supabase = createClient(
@ -51,46 +63,9 @@ if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) {
console.log("Supabase not configured - community features will be limited"); console.log("Supabase not configured - community features will be limited");
} }
client.commands = new Collection(); // =============================================================================
// SENTINEL: HEAT TRACKING SYSTEM (New)
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 heatMap = new Map();
const HEAT_THRESHOLD = 3; const HEAT_THRESHOLD = 3;
@ -125,6 +100,10 @@ client.addHeat = addHeat;
client.getHeat = getHeat; client.getHeat = getHeat;
client.HEAT_THRESHOLD = HEAT_THRESHOLD; client.HEAT_THRESHOLD = HEAT_THRESHOLD;
// =============================================================================
// SENTINEL: FEDERATION MAPPINGS (New)
// =============================================================================
const federationMappings = new Map(); const federationMappings = new Map();
client.federationMappings = federationMappings; client.federationMappings = federationMappings;
@ -137,9 +116,17 @@ const REALM_GUILDS = {
}; };
client.REALM_GUILDS = REALM_GUILDS; client.REALM_GUILDS = REALM_GUILDS;
// =============================================================================
// SENTINEL: TICKET TRACKING (New)
// =============================================================================
const activeTickets = new Map(); const activeTickets = new Map();
client.activeTickets = activeTickets; client.activeTickets = activeTickets;
// =============================================================================
// SENTINEL: ALERT SYSTEM (New)
// =============================================================================
let alertChannelId = process.env.ALERT_CHANNEL_ID; let alertChannelId = process.env.ALERT_CHANNEL_ID;
client.alertChannelId = alertChannelId; client.alertChannelId = alertChannelId;
@ -160,30 +147,88 @@ async function sendAlert(message, embed = null) {
} }
client.sendAlert = sendAlert; client.sendAlert = sendAlert;
// =============================================================================
// COMMAND LOADING
// =============================================================================
client.commands = new Collection();
const commandsPath = path.join(__dirname, "commands");
if (fs.existsSync(commandsPath)) {
const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js"));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
if ("data" in command && "execute" in command) {
client.commands.set(command.data.name, command);
console.log(`Loaded command: ${command.data.name}`);
}
}
}
// =============================================================================
// EVENT LOADING
// =============================================================================
const eventsPath = path.join(__dirname, "events");
if (fs.existsSync(eventsPath)) {
const eventFiles = fs.readdirSync(eventsPath).filter((file) => file.endsWith(".js"));
for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
const event = require(filePath);
if ("name" in event && "execute" in event) {
client.on(event.name, (...args) => event.execute(...args, client, supabase));
console.log(`Loaded event: ${event.name}`);
}
}
}
// =============================================================================
// SENTINEL LISTENER LOADING (New)
// =============================================================================
const sentinelPath = path.join(__dirname, "listeners", "sentinel");
if (fs.existsSync(sentinelPath)) {
const sentinelFiles = fs.readdirSync(sentinelPath).filter((file) => file.endsWith(".js"));
for (const file of sentinelFiles) {
const filePath = path.join(sentinelPath, file);
const listener = require(filePath);
if ("name" in listener && "execute" in listener) {
client.on(listener.name, (...args) => listener.execute(...args, client));
console.log(`Loaded sentinel listener: ${listener.name}`);
}
}
}
// =============================================================================
// FEED SYNC SETUP (Modified: Guard for missing Supabase)
// =============================================================================
let feedSyncModule = null; let feedSyncModule = null;
let setupFeedListener = null;
let sendPostToDiscord = null;
let getFeedChannelId = () => null;
try { try {
feedSyncModule = require("./listeners/feedSync"); feedSyncModule = require("./listeners/feedSync");
setupFeedListener = feedSyncModule.setupFeedListener;
sendPostToDiscord = feedSyncModule.sendPostToDiscord;
getFeedChannelId = feedSyncModule.getFeedChannelId;
} catch (e) { } catch (e) {
console.log("Feed sync module not available"); console.log("Feed sync module not available");
} }
client.once("ready", () => { // =============================================================================
console.log(`Bot logged in as ${client.user.tag}`); // INTERACTION HANDLER (Modified: Added button handling for tickets)
console.log(`Watching ${client.guilds.cache.size} server(s)`); // =============================================================================
client.user.setActivity("Protecting the Federation", { type: 3 });
if (feedSyncModule && feedSyncModule.setupFeedListener && supabase) {
feedSyncModule.setupFeedListener(client);
}
sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`);
});
client.on("interactionCreate", async (interaction) => { client.on("interactionCreate", async (interaction) => {
if (interaction.isChatInputCommand()) { if (interaction.isChatInputCommand()) {
const command = client.commands.get(interaction.commandName); const command = client.commands.get(interaction.commandName);
if (!command) return; if (!command) {
console.warn(`No command matching ${interaction.commandName} was found.`);
return;
}
try { try {
await command.execute(interaction, supabase, client); await command.execute(interaction, supabase, client);
@ -192,7 +237,8 @@ client.on("interactionCreate", async (interaction) => {
const errorEmbed = new EmbedBuilder() const errorEmbed = new EmbedBuilder()
.setColor(0xff0000) .setColor(0xff0000)
.setTitle("Command Error") .setTitle("Command Error")
.setDescription("There was an error while executing this command."); .setDescription("There was an error while executing this command.")
.setFooter({ text: "Contact support if this persists" });
if (interaction.replied || interaction.deferred) { if (interaction.replied || interaction.deferred) {
await interaction.followUp({ embeds: [errorEmbed], ephemeral: true }); await interaction.followUp({ embeds: [errorEmbed], ephemeral: true });
@ -222,54 +268,710 @@ client.on("interactionCreate", async (interaction) => {
} }
}); });
// =============================================================================
// COMMANDS FOR REGISTRATION (Modified: Added Sentinel commands)
// =============================================================================
const COMMANDS_TO_REGISTER = [
{
name: "verify",
description: "Link your Discord account to AeThex",
},
{
name: "set-realm",
description: "Choose your primary arm/realm (Labs, GameForge, Corp, etc.)",
options: [
{
name: "realm",
type: 3,
description: "Your primary realm",
required: true,
choices: [
{ name: "Labs", value: "labs" },
{ name: "GameForge", value: "gameforge" },
{ name: "Corp", value: "corp" },
{ name: "Foundation", value: "foundation" },
{ name: "Dev-Link", value: "devlink" },
],
},
],
},
{
name: "profile",
description: "View your linked AeThex profile",
},
{
name: "unlink",
description: "Disconnect your Discord account from AeThex",
},
{
name: "verify-role",
description: "Check your assigned Discord roles",
},
{
name: "help",
description: "View all AeThex bot commands and features",
},
{
name: "stats",
description: "View your AeThex statistics and activity",
},
{
name: "leaderboard",
description: "View the top AeThex contributors",
options: [
{
name: "category",
type: 3,
description: "Leaderboard category",
required: false,
choices: [
{ name: "Most Active (Posts)", value: "posts" },
{ name: "Most Liked", value: "likes" },
{ name: "Top Creators", value: "creators" },
],
},
],
},
{
name: "post",
description: "Create a post in the AeThex community feed",
options: [
{
name: "content",
type: 3,
description: "Your post content",
required: true,
max_length: 500,
},
{
name: "category",
type: 3,
description: "Post category",
required: false,
choices: [
{ name: "General", value: "general" },
{ name: "Project Update", value: "project_update" },
{ name: "Question", value: "question" },
{ name: "Idea", value: "idea" },
{ name: "Announcement", value: "announcement" },
],
},
{
name: "image",
type: 11,
description: "Attach an image to your post",
required: false,
},
],
},
{
name: "refresh-roles",
description: "Refresh your Discord roles based on your AeThex profile",
},
// Sentinel Commands
{
name: "admin",
description: "Admin controls for bot management",
options: [
{
name: "action",
type: 3,
description: "Admin action to perform",
required: true,
choices: [
{ name: "Status", value: "status" },
{ name: "Heat Check", value: "heat" },
{ name: "Servers", value: "servers" },
{ name: "Threats", value: "threats" },
{ name: "Federation", value: "federation" },
],
},
{
name: "user",
type: 6,
description: "Target user (for heat check)",
required: false,
},
],
},
{
name: "federation",
description: "Manage federation role sync",
options: [
{
name: "action",
type: 3,
description: "Federation action",
required: true,
choices: [
{ name: "Link Role", value: "link" },
{ name: "Unlink Role", value: "unlink" },
{ name: "List Linked", value: "list" },
],
},
{
name: "role",
type: 8,
description: "Role to link/unlink",
required: false,
},
],
},
{
name: "status",
description: "View network status and bot information",
},
{
name: "ticket",
description: "Create or close support tickets",
options: [
{
name: "action",
type: 3,
description: "Ticket action",
required: true,
choices: [
{ name: "Create", value: "create" },
{ name: "Close", value: "close" },
],
},
{
name: "reason",
type: 3,
description: "Reason for ticket (when creating)",
required: false,
},
],
},
];
// =============================================================================
// COMMAND REGISTRATION FUNCTION
// =============================================================================
async function registerDiscordCommands() {
try {
const rest = new REST({ version: "10" }).setToken(
process.env.DISCORD_BOT_TOKEN,
);
console.log(
`Registering ${COMMANDS_TO_REGISTER.length} slash commands...`,
);
try {
const data = await rest.put(
Routes.applicationCommands(process.env.DISCORD_CLIENT_ID),
{ body: COMMANDS_TO_REGISTER },
);
console.log(`Successfully registered ${data.length} slash commands`);
return { success: true, count: data.length, results: null };
} catch (bulkError) {
if (bulkError.code === 50240) {
console.warn(
"Error 50240: Entry Point detected. Registering individually...",
);
const results = [];
let successCount = 0;
let skipCount = 0;
for (const command of COMMANDS_TO_REGISTER) {
try {
const posted = await rest.post(
Routes.applicationCommands(process.env.DISCORD_CLIENT_ID),
{ body: command },
);
results.push({
name: command.name,
status: "registered",
id: posted.id,
});
successCount++;
} catch (postError) {
if (postError.code === 50045) {
results.push({
name: command.name,
status: "already_exists",
});
skipCount++;
} else {
results.push({
name: command.name,
status: "error",
error: postError.message,
});
}
}
}
console.log(
`Registration complete: ${successCount} new, ${skipCount} already existed`,
);
return {
success: true,
count: successCount,
skipped: skipCount,
results,
};
}
throw bulkError;
}
} catch (error) {
console.error("Failed to register commands:", error);
return { success: false, error: error.message };
}
}
// =============================================================================
// HTTP SERVER (Modified: Added Sentinel stats to health endpoint)
// =============================================================================
const healthPort = process.env.HEALTH_PORT || 8080; const healthPort = process.env.HEALTH_PORT || 8080;
const ADMIN_TOKEN = process.env.DISCORD_ADMIN_TOKEN || "aethex-bot-admin";
http.createServer((req, res) => { const checkAdminAuth = (req) => {
res.setHeader("Access-Control-Allow-Origin", "*"); const authHeader = req.headers.authorization;
res.setHeader("Content-Type", "application/json"); return authHeader === `Bearer ${ADMIN_TOKEN}`;
};
if (req.url === "/health") { http
res.writeHead(200); .createServer((req, res) => {
res.end(JSON.stringify({ res.setHeader("Access-Control-Allow-Origin", "*");
status: "online", res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
guilds: client.guilds.cache.size, res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
commands: client.commands.size, res.setHeader("Content-Type", "application/json");
uptime: Math.floor(process.uptime()),
heatMapSize: heatMap.size,
supabaseConnected: !!supabase,
timestamp: new Date().toISOString(),
}));
return;
}
if (req.url === "/stats") { if (req.method === "OPTIONS") {
const guildStats = client.guilds.cache.map(g => ({ res.writeHead(200);
id: g.id, res.end();
name: g.name, return;
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); if (req.url === "/health") {
res.end(JSON.stringify({ error: "Not found" })); res.writeHead(200);
}).listen(healthPort, () => { res.end(
console.log(`Health server running on port ${healthPort}`); JSON.stringify({
}); status: "online",
guilds: client.guilds.cache.size,
commands: client.commands.size,
uptime: Math.floor(process.uptime()),
heatMapSize: heatMap.size,
supabaseConnected: !!supabase,
timestamp: new Date().toISOString(),
}),
);
return;
}
if (req.url === "/stats") {
const guildStats = client.guilds.cache.map(g => ({
id: g.id,
name: g.name,
memberCount: g.memberCount,
}));
res.writeHead(200);
res.end(JSON.stringify({
guilds: guildStats,
totalMembers: guildStats.reduce((sum, g) => sum + g.memberCount, 0),
uptime: Math.floor(process.uptime()),
activeTickets: activeTickets.size,
heatEvents: heatMap.size,
}));
return;
}
if (req.url === "/bot-status") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
const channelId = getFeedChannelId();
const guilds = client.guilds.cache.map((guild) => ({
id: guild.id,
name: guild.name,
memberCount: guild.memberCount,
icon: guild.iconURL(),
}));
res.writeHead(200);
res.end(
JSON.stringify({
status: client.isReady() ? "online" : "offline",
bot: {
tag: client.user?.tag || "Not logged in",
id: client.user?.id,
avatar: client.user?.displayAvatarURL(),
},
guilds: guilds,
guildCount: client.guilds.cache.size,
commands: Array.from(client.commands.keys()),
commandCount: client.commands.size,
uptime: Math.floor(process.uptime()),
feedBridge: {
enabled: !!channelId,
channelId: channelId,
},
sentinel: {
heatMapSize: heatMap.size,
activeTickets: activeTickets.size,
federationMappings: federationMappings.size,
},
supabaseConnected: !!supabase,
timestamp: new Date().toISOString(),
}),
);
return;
}
if (req.url === "/linked-users") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
if (!supabase) {
res.writeHead(200);
res.end(JSON.stringify({ success: true, links: [], count: 0, message: "Supabase not configured" }));
return;
}
(async () => {
try {
const { data: links, error } = await supabase
.from("discord_links")
.select("discord_id, user_id, primary_arm, created_at")
.order("created_at", { ascending: false })
.limit(50);
if (error) throw error;
const enrichedLinks = await Promise.all(
(links || []).map(async (link) => {
const { data: profile } = await supabase
.from("user_profiles")
.select("username, avatar_url")
.eq("id", link.user_id)
.single();
return {
discord_id: link.discord_id.slice(0, 6) + "***",
user_id: link.user_id.slice(0, 8) + "...",
primary_arm: link.primary_arm,
created_at: link.created_at,
profile: profile ? {
username: profile.username,
avatar_url: profile.avatar_url,
} : null,
};
})
);
res.writeHead(200);
res.end(JSON.stringify({ success: true, links: enrichedLinks, count: enrichedLinks.length }));
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ success: false, error: error.message }));
}
})();
return;
}
if (req.url === "/command-stats") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
const stats = {
commands: COMMANDS_TO_REGISTER.map((cmd) => ({
name: cmd.name,
description: cmd.description,
options: cmd.options?.length || 0,
})),
totalCommands: COMMANDS_TO_REGISTER.length,
};
res.writeHead(200);
res.end(JSON.stringify({ success: true, stats }));
return;
}
if (req.url === "/feed-stats") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
if (!supabase) {
res.writeHead(200);
res.end(JSON.stringify({ success: true, stats: { totalPosts: 0, discordPosts: 0, websitePosts: 0, recentPosts: [] }, message: "Supabase not configured" }));
return;
}
(async () => {
try {
const { count: totalPosts } = await supabase
.from("community_posts")
.select("*", { count: "exact", head: true });
const { count: discordPosts } = await supabase
.from("community_posts")
.select("*", { count: "exact", head: true })
.eq("source", "discord");
const { count: websitePosts } = await supabase
.from("community_posts")
.select("*", { count: "exact", head: true })
.or("source.is.null,source.neq.discord");
const { data: recentPosts } = await supabase
.from("community_posts")
.select("id, content, source, created_at")
.order("created_at", { ascending: false })
.limit(10);
res.writeHead(200);
res.end(
JSON.stringify({
success: true,
stats: {
totalPosts: totalPosts || 0,
discordPosts: discordPosts || 0,
websitePosts: websitePosts || 0,
recentPosts: (recentPosts || []).map(p => ({
id: p.id,
content: p.content?.slice(0, 100) + (p.content?.length > 100 ? "..." : ""),
source: p.source,
created_at: p.created_at,
})),
},
})
);
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ success: false, error: error.message }));
}
})();
return;
}
if (req.url === "/send-to-discord" && req.method === "POST") {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", async () => {
try {
const authHeader = req.headers.authorization;
const expectedToken = process.env.DISCORD_BRIDGE_TOKEN || "aethex-bridge";
if (authHeader !== `Bearer ${expectedToken}`) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized" }));
return;
}
const post = JSON.parse(body);
console.log("[API] Received post to send to Discord:", post.id);
if (sendPostToDiscord) {
const result = await sendPostToDiscord(post, post.author);
res.writeHead(result.success ? 200 : 500);
res.end(JSON.stringify(result));
} else {
res.writeHead(500);
res.end(JSON.stringify({ error: "Feed sync not available" }));
}
} catch (error) {
console.error("[API] Error processing send-to-discord:", error);
res.writeHead(500);
res.end(JSON.stringify({ error: error.message }));
}
});
return;
}
if (req.url === "/bridge-status") {
const channelId = getFeedChannelId();
res.writeHead(200);
res.end(
JSON.stringify({
enabled: !!channelId,
channelId: channelId,
botReady: client.isReady(),
}),
);
return;
}
if (req.url === "/register-commands") {
if (req.method === "GET") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
res.writeHead(200, { "Content-Type": "text/html" });
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>Register Discord Commands</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
text-align: center;
max-width: 500px;
}
h1 { color: #333; margin-bottom: 20px; }
p { color: #666; margin-bottom: 30px; }
button {
background: #667eea;
color: white;
border: none;
padding: 12px 30px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: background 0.3s;
}
button:hover { background: #764ba2; }
button:disabled { background: #ccc; cursor: not-allowed; }
#result { margin-top: 30px; padding: 20px; border-radius: 5px; display: none; }
#result.success { background: #d4edda; color: #155724; display: block; }
#result.error { background: #f8d7da; color: #721c24; display: block; }
#loading { display: none; color: #667eea; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<h1>Discord Commands Registration</h1>
<p>Click to register all ${COMMANDS_TO_REGISTER.length} slash commands</p>
<button id="registerBtn" onclick="registerCommands()">Register Commands</button>
<div id="loading">Registering... please wait...</div>
<div id="result"></div>
</div>
<script>
async function registerCommands() {
const btn = document.getElementById('registerBtn');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
btn.disabled = true;
loading.style.display = 'block';
result.style.display = 'none';
try {
const response = await fetch('/register-commands', {
method: 'POST',
headers: { 'Authorization': 'Bearer ${ADMIN_TOKEN}', 'Content-Type': 'application/json' }
});
const data = await response.json();
loading.style.display = 'none';
if (response.ok && data.success) {
result.className = 'success';
result.innerHTML = '<h3>Success!</h3><p>Registered ' + data.count + ' commands</p>';
} else {
result.className = 'error';
result.innerHTML = '<h3>Error</h3><p>' + (data.error || 'Failed') + '</p>';
}
} catch (error) {
loading.style.display = 'none';
result.className = 'error';
result.innerHTML = '<h3>Error</h3><p>' + error.message + '</p>';
} finally {
btn.disabled = false;
}
}
</script>
</body>
</html>
`);
return;
}
if (req.method === "POST") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
registerDiscordCommands().then((result) => {
if (result.success) {
res.writeHead(200);
res.end(JSON.stringify(result));
} else {
res.writeHead(500);
res.end(JSON.stringify(result));
}
});
return;
}
}
res.writeHead(404);
res.end(JSON.stringify({ error: "Not found" }));
})
.listen(healthPort, () => {
console.log(`Health check server running on port ${healthPort}`);
console.log(`Register commands at: POST http://localhost:${healthPort}/register-commands`);
});
// =============================================================================
// BOT LOGIN AND READY
// =============================================================================
client.login(token).catch((error) => { client.login(token).catch((error) => {
console.error("Failed to login:", error.message); console.error("Failed to login to Discord");
console.error(`Error Code: ${error.code}`);
console.error(`Error Message: ${error.message}`);
if (error.code === "TokenInvalid") {
console.error("\nDISCORD_BOT_TOKEN is invalid!");
console.error("Get a new token from: https://discord.com/developers/applications");
}
process.exit(1); process.exit(1);
}); });
client.once("ready", () => {
console.log(`Bot logged in as ${client.user.tag}`);
console.log(`Watching ${client.guilds.cache.size} server(s)`);
console.log("Commands are registered via: npm run register-commands");
client.user.setActivity("Protecting the Federation", { type: 3 });
if (setupFeedListener && supabase) {
setupFeedListener(client);
}
sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`);
});
// =============================================================================
// ERROR HANDLING
// =============================================================================
process.on("unhandledRejection", (error) => { process.on("unhandledRejection", (error) => {
console.error("Unhandled Promise Rejection:", error); console.error("Unhandled Promise Rejection:", error);
}); });

View file

@ -17,15 +17,10 @@ module.exports = {
), ),
async execute(interaction, supabase) { async execute(interaction, supabase) {
await interaction.deferReply();
if (!supabase) { if (!supabase) {
const embed = new EmbedBuilder() return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
.setColor(0xff6b6b)
.setTitle("⚠️ Feature Unavailable")
.setDescription("Leaderboard is not configured. Contact an administrator.");
return await interaction.editReply({ embeds: [embed] });
} }
await interaction.deferReply();
try { try {
const category = interaction.options.getString("category") || "posts"; const category = interaction.options.getString("category") || "posts";

View file

@ -39,15 +39,10 @@ module.exports = {
), ),
async execute(interaction, supabase, client) { async execute(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
if (!supabase) { if (!supabase) {
const embed = new EmbedBuilder() return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
.setColor(0xff6b6b)
.setTitle("⚠️ Feature Unavailable")
.setDescription("Posting is not configured. Contact an administrator.");
return await interaction.editReply({ embeds: [embed] });
} }
await interaction.deferReply({ ephemeral: true });
try { try {
const { data: link } = await supabase const { data: link } = await supabase

View file

@ -6,15 +6,10 @@ module.exports = {
.setDescription("View your AeThex profile in Discord"), .setDescription("View your AeThex profile in Discord"),
async execute(interaction, supabase) { async execute(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
if (!supabase) { if (!supabase) {
const embed = new EmbedBuilder() return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
.setColor(0xff6b6b)
.setTitle("⚠️ Feature Unavailable")
.setDescription("Profile features are not configured. Contact an administrator.");
return await interaction.editReply({ embeds: [embed] });
} }
await interaction.deferReply({ ephemeral: true });
try { try {
const { data: link } = await supabase const { data: link } = await supabase

View file

@ -9,17 +9,13 @@ module.exports = {
), ),
async execute(interaction, supabase, client) { async execute(interaction, supabase, client) {
if (!supabase) {
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
}
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ ephemeral: true });
if (!supabase) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("⚠️ Feature Unavailable")
.setDescription("Role sync is not configured. Contact an administrator.");
return await interaction.editReply({ embeds: [embed] });
}
try { try {
// Check if user is linked
const { data: link } = await supabase const { data: link } = await supabase
.from("discord_links") .from("discord_links")
.select("primary_arm") .select("primary_arm")

View file

@ -32,15 +32,10 @@ module.exports = {
.setDescription("Set your primary AeThex realm/arm"), .setDescription("Set your primary AeThex realm/arm"),
async execute(interaction, supabase, client) { async execute(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
if (!supabase) { if (!supabase) {
const embed = new EmbedBuilder() return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
.setColor(0xff6b6b)
.setTitle("⚠️ Feature Unavailable")
.setDescription("Realm settings are not configured. Contact an administrator.");
return await interaction.editReply({ embeds: [embed] });
} }
await interaction.deferReply({ ephemeral: true });
try { try {
const { data: link } = await supabase const { data: link } = await supabase

View file

@ -6,15 +6,10 @@ module.exports = {
.setDescription("View your AeThex statistics and activity"), .setDescription("View your AeThex statistics and activity"),
async execute(interaction, supabase) { async execute(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
if (!supabase) { if (!supabase) {
const embed = new EmbedBuilder() return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
.setColor(0xff6b6b)
.setTitle("⚠️ Feature Unavailable")
.setDescription("Stats are not configured. Contact an administrator.");
return await interaction.editReply({ embeds: [embed] });
} }
await interaction.deferReply({ ephemeral: true });
try { try {
const { data: link } = await supabase const { data: link } = await supabase

View file

@ -6,15 +6,10 @@ module.exports = {
.setDescription("Unlink your Discord account from AeThex"), .setDescription("Unlink your Discord account from AeThex"),
async execute(interaction, supabase) { async execute(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
if (!supabase) { if (!supabase) {
const embed = new EmbedBuilder() return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
.setColor(0xff6b6b)
.setTitle("⚠️ Feature Unavailable")
.setDescription("Account unlinking is not configured. Contact an administrator.");
return await interaction.editReply({ embeds: [embed] });
} }
await interaction.deferReply({ ephemeral: true });
try { try {
const { data: link } = await supabase const { data: link } = await supabase

View file

@ -6,15 +6,10 @@ module.exports = {
.setDescription("Check your AeThex-assigned Discord roles"), .setDescription("Check your AeThex-assigned Discord roles"),
async execute(interaction, supabase) { async execute(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
if (!supabase) { if (!supabase) {
const embed = new EmbedBuilder() return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
.setColor(0xff6b6b)
.setTitle("⚠️ Feature Unavailable")
.setDescription("Role verification is not configured. Contact an administrator.");
return await interaction.editReply({ embeds: [embed] });
} }
await interaction.deferReply({ ephemeral: true });
try { try {
const { data: link } = await supabase const { data: link } = await supabase

View file

@ -13,15 +13,10 @@ module.exports = {
.setDescription("Link your Discord account to your AeThex account"), .setDescription("Link your Discord account to your AeThex account"),
async execute(interaction, supabase, client) { async execute(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
if (!supabase) { if (!supabase) {
const embed = new EmbedBuilder() return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
.setColor(0xff6b6b)
.setTitle("⚠️ Feature Unavailable")
.setDescription("Account linking is not configured. Contact an administrator.");
return await interaction.editReply({ embeds: [embed] });
} }
await interaction.deferReply({ ephemeral: true });
try { try {
const { data: existingLink } = await supabase const { data: existingLink } = await supabase

View file

@ -0,0 +1,10 @@
TYPE=bot
MAIN=bot.js
NAME=AeThex
AVATAR=https://docs.aethex.tech/~gitbook/image?url=https%3A%2F%2F1143808467-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Forganizations%252FDhUg3jal6kdpG645FzIl%252Fsites%252Fsite_HeOmR%252Flogo%252FqxDYz8Oj2SnwUTa8t3UB%252FAeThex%2520Origin%2520logo.png%3Falt%3Dmedia%26token%3D200e8ea2-0129-4cbe-b516-4a53f60c512b&width=512&dpr=1&quality=100&sign=6c7576ce&sv=2
RAM=100
AUTORESTART=true
APT=tool, education, gamedev
START=npm install
BUILD=npm run build
VLAN=true

View file

@ -1,10 +1,13 @@
const { EmbedBuilder } = require("discord.js"); const { EmbedBuilder } = require("discord.js");
const { createClient } = require("@supabase/supabase-js"); const { createClient } = require("@supabase/supabase-js");
const supabase = createClient( let supabase = null;
process.env.SUPABASE_URL, if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) {
process.env.SUPABASE_SERVICE_ROLE, supabase = createClient(
); process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE,
);
}
const FEED_CHANNEL_ID = process.env.DISCORD_MAIN_CHAT_CHANNELS const FEED_CHANNEL_ID = process.env.DISCORD_MAIN_CHAT_CHANNELS
? process.env.DISCORD_MAIN_CHAT_CHANNELS.split(",")[0].trim() ? process.env.DISCORD_MAIN_CHAT_CHANNELS.split(",")[0].trim()
@ -207,6 +210,11 @@ async function checkForNewPosts() {
function setupFeedListener(client) { function setupFeedListener(client) {
discordClient = client; discordClient = client;
if (!supabase) {
console.log("[Feed Bridge] No Supabase configured - bridge disabled");
return;
}
if (!FEED_CHANNEL_ID) { if (!FEED_CHANNEL_ID) {
console.log("[Feed Bridge] No DISCORD_MAIN_CHAT_CHANNELS configured - bridge disabled"); console.log("[Feed Bridge] No DISCORD_MAIN_CHAT_CHANNELS configured - bridge disabled");
return; return;

View file

@ -7,7 +7,9 @@
"": { "": {
"name": "aethex-unified-bot", "name": "aethex-unified-bot",
"version": "2.0.0", "version": "2.0.0",
"license": "MIT",
"dependencies": { "dependencies": {
"@discord/embedded-app-sdk": "^2.4.0",
"@supabase/supabase-js": "^2.38.0", "@supabase/supabase-js": "^2.38.0",
"axios": "^1.6.0", "axios": "^1.6.0",
"discord.js": "^14.13.0", "discord.js": "^14.13.0",
@ -20,6 +22,22 @@
"node": ">=20.0.0" "node": ">=20.0.0"
} }
}, },
"node_modules/@discord/embedded-app-sdk": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@discord/embedded-app-sdk/-/embedded-app-sdk-2.4.0.tgz",
"integrity": "sha512-kIIS79tuVKvu9YC6GIuvBSfUqNa6511UqafD4i3qGjWSRqVulioYuRzZ+M9D9/KZ2wuu0nQ5IWIYlnh1bsy2tg==",
"license": "MIT",
"dependencies": {
"@types/lodash.transform": "^4.6.6",
"@types/uuid": "^10.0.0",
"big-integer": "^1.6.48",
"decimal.js-light": "^2.5.0",
"eventemitter3": "^5.0.0",
"lodash.transform": "^4.6.0",
"uuid": "^11.0.0",
"zod": "^3.9.8"
}
},
"node_modules/@discordjs/builders": { "node_modules/@discordjs/builders": {
"version": "1.13.1", "version": "1.13.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz", "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz",
@ -263,6 +281,21 @@
"node": ">=20.0.0" "node": ">=20.0.0"
} }
}, },
"node_modules/@types/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==",
"license": "MIT"
},
"node_modules/@types/lodash.transform": {
"version": "4.6.9",
"resolved": "https://registry.npmjs.org/@types/lodash.transform/-/lodash.transform-4.6.9.tgz",
"integrity": "sha512-1iIn+l7Vrj8hsr2iZLtxRkcV9AtjTafIyxKO9DX2EEcdOgz3Op5dhwKQFhMJgdfIRbYHBUF+SU97Y6P+zyLXNg==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.10.1", "version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
@ -278,6 +311,12 @@
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.18.1", "version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@ -335,6 +374,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
"license": "Unlicense",
"engines": {
"node": ">=0.6"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -447,6 +495,12 @@
} }
} }
}, },
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -563,6 +617,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -827,6 +887,12 @@
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.transform": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz",
"integrity": "sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ==",
"license": "MIT"
},
"node_modules/magic-bytes.js": { "node_modules/magic-bytes.js": {
"version": "1.12.1", "version": "1.12.1",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz",
@ -1057,6 +1123,19 @@
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/ws": { "node_modules/ws": {
"version": "8.18.3", "version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
@ -1077,6 +1156,15 @@
"optional": true "optional": true
} }
} }
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }

View file

@ -9,7 +9,19 @@
"dev": "nodemon bot.js", "dev": "nodemon bot.js",
"register-commands": "node scripts/register-commands.js" "register-commands": "node scripts/register-commands.js"
}, },
"keywords": [
"discord",
"bot",
"aethex",
"role-management",
"sentinel",
"security",
"discord.js"
],
"author": "AeThex Team",
"license": "MIT",
"dependencies": { "dependencies": {
"@discord/embedded-app-sdk": "^2.4.0",
"@supabase/supabase-js": "^2.38.0", "@supabase/supabase-js": "^2.38.0",
"axios": "^1.6.0", "axios": "^1.6.0",
"discord.js": "^14.13.0", "discord.js": "^14.13.0",

View file

@ -1,11 +1,12 @@
# AeThex Unified Bot # AeThex Unified Bot
A single Discord bot combining community features and enterprise security (Sentinel). A complete Discord bot combining AeThex community features and Sentinel enterprise security in one instance.
## Overview ## Overview
AeThex Unified Bot handles both community features AND security in one instance: AeThex Unified Bot handles both community features AND security:
- **Community Features**: User verification, profile linking, realm selection, leaderboards, community posts
- **Sentinel Security**: Anti-nuke protection with RAM-based heat tracking - **Sentinel Security**: Anti-nuke protection with RAM-based heat tracking
- **Federation Sync**: Cross-server role synchronization across 5 realms - **Federation Sync**: Cross-server role synchronization across 5 realms
- **Ticket System**: Support tickets with automatic channel creation - **Ticket System**: Support tickets with automatic channel creation
@ -15,24 +16,38 @@ AeThex Unified Bot handles both community features AND security in one instance:
- **Runtime**: Node.js 20 - **Runtime**: Node.js 20
- **Framework**: discord.js v14 - **Framework**: discord.js v14
- **Database**: Supabase (optional, for user verification) - **Database**: Supabase (optional - for user verification and community features)
- **Health Endpoint**: HTTP server on port 8080 - **Health Endpoint**: HTTP server on port 8080
## Project Structure ## Project Structure
``` ```
aethex-bot/ aethex-bot/
├── bot.js # Main entry point ├── bot.js # Main entry point (merged: original + Sentinel)
├── package.json ├── package.json
├── .env.example ├── .env.example # Complete environment template
├── Dockerfile # Docker deployment config
├── discloud.config # DisCloud hosting config
├── DEPLOYMENT_GUIDE.md # Deployment documentation
├── commands/ ├── commands/
│ ├── admin.js # /admin status|heat|servers|threats|federation │ ├── admin.js # /admin status|heat|servers|threats|federation
│ ├── federation.js # /federation link|unlink|list │ ├── federation.js # /federation link|unlink|list
│ ├── help.js # /help - command list
│ ├── leaderboard.js # /leaderboard - top contributors
│ ├── post.js # /post - community feed posts
│ ├── profile.js # /profile - view linked profile
│ ├── refresh-roles.js # /refresh-roles - sync roles
│ ├── set-realm.js # /set-realm - choose primary realm
│ ├── stats.js # /stats - user statistics
│ ├── status.js # /status - network overview │ ├── status.js # /status - network overview
│ └── ticket.js # /ticket create|close │ ├── ticket.js # /ticket create|close
│ ├── unlink.js # /unlink - disconnect account
│ ├── verify-role.js # /verify-role - check roles
│ └── verify.js # /verify - link account
├── events/ ├── events/
│ └── guildMemberUpdate.js # Federation role sync listener │ └── messageCreate.js # Message event handler
├── listeners/ ├── listeners/
│ ├── feedSync.js # Community feed sync
│ └── sentinel/ │ └── sentinel/
│ ├── antiNuke.js # Channel delete monitor │ ├── antiNuke.js # Channel delete monitor
│ ├── roleDelete.js # Role delete monitor │ ├── roleDelete.js # Role delete monitor
@ -42,8 +57,23 @@ aethex-bot/
└── register-commands.js # Slash command registration └── register-commands.js # Slash command registration
``` ```
## Commands ## Commands (14 Total)
### Community Commands (10)
| Command | Description |
|---------|-------------|
| `/verify` | Link your Discord account to AeThex |
| `/unlink` | Disconnect your Discord from AeThex |
| `/profile` | View your linked AeThex profile |
| `/set-realm` | Choose your primary realm |
| `/verify-role` | Check your assigned Discord roles |
| `/refresh-roles` | Sync roles based on AeThex profile |
| `/stats` | View your AeThex statistics |
| `/leaderboard` | View top contributors |
| `/post` | Create a community feed post |
| `/help` | View all bot commands |
### Sentinel Commands (4)
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `/admin status` | View bot status and statistics | | `/admin status` | View bot status and statistics |
@ -60,7 +90,7 @@ aethex-bot/
## Sentinel Security System ## Sentinel Security System
The anti-nuke system uses RAM-based heat tracking for instant response: Anti-nuke system using RAM-based heat tracking for instant response:
- **Heat Threshold**: 3 dangerous actions in 10 seconds triggers auto-ban - **Heat Threshold**: 3 dangerous actions in 10 seconds triggers auto-ban
- **Monitored Actions**: Channel delete, role delete, member ban, member kick - **Monitored Actions**: Channel delete, role delete, member ban, member kick
@ -69,32 +99,39 @@ The anti-nuke system uses RAM-based heat tracking for instant response:
## Environment Variables ## Environment Variables
Required: ### Required
- `DISCORD_TOKEN` or `DISCORD_BOT_TOKEN` - Bot token - `DISCORD_BOT_TOKEN` - Bot token from Discord Developer Portal
- `DISCORD_CLIENT_ID` - Application ID (currently: 578971245454950421) - `DISCORD_CLIENT_ID` - Application ID (e.g., 578971245454950421)
Optional - Federation: ### Optional - Supabase (for community features)
- `SUPABASE_URL` - Supabase project URL
- `SUPABASE_SERVICE_ROLE` - Supabase service role key
### Optional - Federation
- `HUB_GUILD_ID` - Main hub server - `HUB_GUILD_ID` - Main hub server
- `LABS_GUILD_ID`, `GAMEFORGE_GUILD_ID`, `CORP_GUILD_ID`, `FOUNDATION_GUILD_ID` - `LABS_GUILD_ID`, `GAMEFORGE_GUILD_ID`, `CORP_GUILD_ID`, `FOUNDATION_GUILD_ID`
Optional - Security: ### Optional - Security
- `WHITELISTED_USERS` - Comma-separated user IDs to skip heat tracking - `WHITELISTED_USERS` - Comma-separated user IDs to skip heat tracking
- `ALERT_CHANNEL_ID` - Channel for security alerts - `ALERT_CHANNEL_ID` - Channel for security alerts
Optional - Supabase: ### Optional - Feed Sync
- `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE` - For user verification features - `DISCORD_FEED_CHANNEL_ID` - Channel for community feed
- `DISCORD_FEED_GUILD_ID` - Guild for community feed
- `DISCORD_MAIN_CHAT_CHANNELS` - Comma-separated channel IDs
## Health Endpoint ## Health Endpoints
**GET /health** (port 8080) **GET /health** (port 8080)
```json ```json
{ {
"status": "online", "status": "online",
"guilds": 5, "guilds": 8,
"commands": 4, "commands": 14,
"uptime": 3600, "uptime": 3600,
"heatMapSize": 0, "heatMapSize": 0,
"timestamp": "2025-12-07T22:15:00.000Z" "supabaseConnected": false,
"timestamp": "2025-12-07T23:00:00.000Z"
} }
``` ```
@ -114,13 +151,22 @@ Optional - Supabase:
```bash ```bash
cd aethex-bot cd aethex-bot
npm install npm install
node scripts/register-commands.js # Register slash commands (run once)
npm start npm start
``` ```
Commands are registered automatically on startup or via POST to `/register-commands`.
## Current Status ## Current Status
- Bot is running and connected to 5 servers - Bot running as AeThex#9389
- All 4 commands registered (/admin, /federation, /status, /ticket) - Connected to 8 servers
- Sentinel listeners active (channel/role delete, ban/kick monitoring) - 14 commands loaded
- Health endpoint available at port 8080 - 4 Sentinel listeners active
- Health endpoint on port 8080
- Supabase optional (community features limited when not configured)
## Workflow
- **Name**: AeThex Unified Bot
- **Command**: `cd aethex-bot && npm start`
- **Status**: Running