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
externalPort = 8080
[[ports]]
localPort = 38431
externalPort = 3000
[workflows]
runButton = "Project"

View file

@ -1,21 +1,48 @@
# Required
DISCORD_BOT_TOKEN=your_discord_bot_token
DISCORD_CLIENT_ID=your_discord_client_id
# Discord Bot Configuration
DISCORD_BOT_TOKEN=your_bot_token_here
DISCORD_CLIENT_ID=your_client_id_here
DISCORD_PUBLIC_KEY=your_public_key_here
# Optional - Supabase (for user verification features)
SUPABASE_URL=your_supabase_url
SUPABASE_SERVICE_ROLE=your_supabase_service_role_key
# Supabase Configuration (optional - community features require this)
SUPABASE_URL=https://your-project.supabase.co
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
LABS_GUILD_ID=labs_server_id
GAMEFORGE_GUILD_ID=gameforge_server_id
CORP_GUILD_ID=corp_server_id
FOUNDATION_GUILD_ID=foundation_server_id
# Optional - Security
# Security Settings
WHITELISTED_USERS=user_id_1,user_id_2
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");
require("dotenv").config();
// =============================================================================
// ENVIRONMENT VALIDATION (Modified: Supabase now optional)
// =============================================================================
const token = process.env.DISCORD_BOT_TOKEN;
const clientId = process.env.DISCORD_CLIENT_ID;
@ -29,6 +33,10 @@ if (!clientId) {
console.log("[Token] Bot token loaded (length: " + token.length + " chars)");
// =============================================================================
// DISCORD CLIENT SETUP (Modified: Added intents for Sentinel)
// =============================================================================
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
@ -40,6 +48,10 @@ const client = new Client({
],
});
// =============================================================================
// SUPABASE SETUP (Modified: Now optional)
// =============================================================================
let supabase = null;
if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) {
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");
}
client.commands = new Collection();
const commandsPath = path.join(__dirname, "commands");
if (fs.existsSync(commandsPath)) {
const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js"));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
if ("data" in command && "execute" in command) {
client.commands.set(command.data.name, command);
console.log(`Loaded command: ${command.data.name}`);
}
}
}
const eventsPath = path.join(__dirname, "events");
if (fs.existsSync(eventsPath)) {
const eventFiles = fs.readdirSync(eventsPath).filter((file) => file.endsWith(".js"));
for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
const event = require(filePath);
if ("name" in event && "execute" in event) {
client.on(event.name, (...args) => event.execute(...args, client, supabase));
console.log(`Loaded event: ${event.name}`);
}
}
}
const sentinelPath = path.join(__dirname, "listeners", "sentinel");
if (fs.existsSync(sentinelPath)) {
const sentinelFiles = fs.readdirSync(sentinelPath).filter((file) => file.endsWith(".js"));
for (const file of sentinelFiles) {
const filePath = path.join(sentinelPath, file);
const listener = require(filePath);
if ("name" in listener && "execute" in listener) {
client.on(listener.name, (...args) => listener.execute(...args, client));
console.log(`Loaded sentinel listener: ${listener.name}`);
}
}
}
// =============================================================================
// SENTINEL: HEAT TRACKING SYSTEM (New)
// =============================================================================
const heatMap = new Map();
const HEAT_THRESHOLD = 3;
@ -125,6 +100,10 @@ client.addHeat = addHeat;
client.getHeat = getHeat;
client.HEAT_THRESHOLD = HEAT_THRESHOLD;
// =============================================================================
// SENTINEL: FEDERATION MAPPINGS (New)
// =============================================================================
const federationMappings = new Map();
client.federationMappings = federationMappings;
@ -137,9 +116,17 @@ const REALM_GUILDS = {
};
client.REALM_GUILDS = REALM_GUILDS;
// =============================================================================
// SENTINEL: TICKET TRACKING (New)
// =============================================================================
const activeTickets = new Map();
client.activeTickets = activeTickets;
// =============================================================================
// SENTINEL: ALERT SYSTEM (New)
// =============================================================================
let alertChannelId = process.env.ALERT_CHANNEL_ID;
client.alertChannelId = alertChannelId;
@ -160,30 +147,88 @@ async function sendAlert(message, embed = null) {
}
client.sendAlert = sendAlert;
// =============================================================================
// COMMAND LOADING
// =============================================================================
client.commands = new Collection();
const commandsPath = path.join(__dirname, "commands");
if (fs.existsSync(commandsPath)) {
const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js"));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
if ("data" in command && "execute" in command) {
client.commands.set(command.data.name, command);
console.log(`Loaded command: ${command.data.name}`);
}
}
}
// =============================================================================
// EVENT LOADING
// =============================================================================
const eventsPath = path.join(__dirname, "events");
if (fs.existsSync(eventsPath)) {
const eventFiles = fs.readdirSync(eventsPath).filter((file) => file.endsWith(".js"));
for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
const event = require(filePath);
if ("name" in event && "execute" in event) {
client.on(event.name, (...args) => event.execute(...args, client, supabase));
console.log(`Loaded event: ${event.name}`);
}
}
}
// =============================================================================
// SENTINEL LISTENER LOADING (New)
// =============================================================================
const sentinelPath = path.join(__dirname, "listeners", "sentinel");
if (fs.existsSync(sentinelPath)) {
const sentinelFiles = fs.readdirSync(sentinelPath).filter((file) => file.endsWith(".js"));
for (const file of sentinelFiles) {
const filePath = path.join(sentinelPath, file);
const listener = require(filePath);
if ("name" in listener && "execute" in listener) {
client.on(listener.name, (...args) => listener.execute(...args, client));
console.log(`Loaded sentinel listener: ${listener.name}`);
}
}
}
// =============================================================================
// FEED SYNC SETUP (Modified: Guard for missing Supabase)
// =============================================================================
let feedSyncModule = null;
let setupFeedListener = null;
let sendPostToDiscord = null;
let getFeedChannelId = () => null;
try {
feedSyncModule = require("./listeners/feedSync");
setupFeedListener = feedSyncModule.setupFeedListener;
sendPostToDiscord = feedSyncModule.sendPostToDiscord;
getFeedChannelId = feedSyncModule.getFeedChannelId;
} catch (e) {
console.log("Feed sync module not available");
}
client.once("ready", () => {
console.log(`Bot logged in as ${client.user.tag}`);
console.log(`Watching ${client.guilds.cache.size} server(s)`);
client.user.setActivity("Protecting the Federation", { type: 3 });
if (feedSyncModule && feedSyncModule.setupFeedListener && supabase) {
feedSyncModule.setupFeedListener(client);
}
sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`);
});
// =============================================================================
// INTERACTION HANDLER (Modified: Added button handling for tickets)
// =============================================================================
client.on("interactionCreate", async (interaction) => {
if (interaction.isChatInputCommand()) {
const command = client.commands.get(interaction.commandName);
if (!command) return;
if (!command) {
console.warn(`No command matching ${interaction.commandName} was found.`);
return;
}
try {
await command.execute(interaction, supabase, client);
@ -192,7 +237,8 @@ client.on("interactionCreate", async (interaction) => {
const errorEmbed = new EmbedBuilder()
.setColor(0xff0000)
.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) {
await interaction.followUp({ embeds: [errorEmbed], ephemeral: true });
@ -222,54 +268,710 @@ client.on("interactionCreate", async (interaction) => {
}
});
const healthPort = process.env.HEALTH_PORT || 8080;
// =============================================================================
// COMMANDS FOR REGISTRATION (Modified: Added Sentinel commands)
// =============================================================================
http.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Content-Type", "application/json");
if (req.url === "/health") {
res.writeHead(200);
res.end(JSON.stringify({
status: "online",
guilds: client.guilds.cache.size,
commands: client.commands.size,
uptime: Math.floor(process.uptime()),
heatMapSize: heatMap.size,
supabaseConnected: !!supabase,
timestamp: new Date().toISOString(),
}));
return;
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 };
}
if (req.url === "/stats") {
const guildStats = client.guilds.cache.map(g => ({
id: g.id,
name: g.name,
memberCount: g.memberCount,
}));
res.writeHead(200);
res.end(JSON.stringify({
guilds: guildStats,
totalMembers: guildStats.reduce((sum, g) => sum + g.memberCount, 0),
uptime: Math.floor(process.uptime()),
activeTickets: activeTickets.size,
heatEvents: heatMap.size,
}));
return;
}
res.writeHead(404);
res.end(JSON.stringify({ error: "Not found" }));
}).listen(healthPort, () => {
console.log(`Health server running on port ${healthPort}`);
});
}
// =============================================================================
// HTTP SERVER (Modified: Added Sentinel stats to health endpoint)
// =============================================================================
const healthPort = process.env.HEALTH_PORT || 8080;
const ADMIN_TOKEN = process.env.DISCORD_ADMIN_TOKEN || "aethex-bot-admin";
const checkAdminAuth = (req) => {
const authHeader = req.headers.authorization;
return authHeader === `Bearer ${ADMIN_TOKEN}`;
};
http
.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Content-Type", "application/json");
if (req.method === "OPTIONS") {
res.writeHead(200);
res.end();
return;
}
if (req.url === "/health") {
res.writeHead(200);
res.end(
JSON.stringify({
status: "online",
guilds: client.guilds.cache.size,
commands: client.commands.size,
uptime: Math.floor(process.uptime()),
heatMapSize: heatMap.size,
supabaseConnected: !!supabase,
timestamp: new Date().toISOString(),
}),
);
return;
}
if (req.url === "/stats") {
const guildStats = client.guilds.cache.map(g => ({
id: g.id,
name: g.name,
memberCount: g.memberCount,
}));
res.writeHead(200);
res.end(JSON.stringify({
guilds: guildStats,
totalMembers: guildStats.reduce((sum, g) => sum + g.memberCount, 0),
uptime: Math.floor(process.uptime()),
activeTickets: activeTickets.size,
heatEvents: heatMap.size,
}));
return;
}
if (req.url === "/bot-status") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
const channelId = getFeedChannelId();
const guilds = client.guilds.cache.map((guild) => ({
id: guild.id,
name: guild.name,
memberCount: guild.memberCount,
icon: guild.iconURL(),
}));
res.writeHead(200);
res.end(
JSON.stringify({
status: client.isReady() ? "online" : "offline",
bot: {
tag: client.user?.tag || "Not logged in",
id: client.user?.id,
avatar: client.user?.displayAvatarURL(),
},
guilds: guilds,
guildCount: client.guilds.cache.size,
commands: Array.from(client.commands.keys()),
commandCount: client.commands.size,
uptime: Math.floor(process.uptime()),
feedBridge: {
enabled: !!channelId,
channelId: channelId,
},
sentinel: {
heatMapSize: heatMap.size,
activeTickets: activeTickets.size,
federationMappings: federationMappings.size,
},
supabaseConnected: !!supabase,
timestamp: new Date().toISOString(),
}),
);
return;
}
if (req.url === "/linked-users") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
if (!supabase) {
res.writeHead(200);
res.end(JSON.stringify({ success: true, links: [], count: 0, message: "Supabase not configured" }));
return;
}
(async () => {
try {
const { data: links, error } = await supabase
.from("discord_links")
.select("discord_id, user_id, primary_arm, created_at")
.order("created_at", { ascending: false })
.limit(50);
if (error) throw error;
const enrichedLinks = await Promise.all(
(links || []).map(async (link) => {
const { data: profile } = await supabase
.from("user_profiles")
.select("username, avatar_url")
.eq("id", link.user_id)
.single();
return {
discord_id: link.discord_id.slice(0, 6) + "***",
user_id: link.user_id.slice(0, 8) + "...",
primary_arm: link.primary_arm,
created_at: link.created_at,
profile: profile ? {
username: profile.username,
avatar_url: profile.avatar_url,
} : null,
};
})
);
res.writeHead(200);
res.end(JSON.stringify({ success: true, links: enrichedLinks, count: enrichedLinks.length }));
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ success: false, error: error.message }));
}
})();
return;
}
if (req.url === "/command-stats") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
const stats = {
commands: COMMANDS_TO_REGISTER.map((cmd) => ({
name: cmd.name,
description: cmd.description,
options: cmd.options?.length || 0,
})),
totalCommands: COMMANDS_TO_REGISTER.length,
};
res.writeHead(200);
res.end(JSON.stringify({ success: true, stats }));
return;
}
if (req.url === "/feed-stats") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
if (!supabase) {
res.writeHead(200);
res.end(JSON.stringify({ success: true, stats: { totalPosts: 0, discordPosts: 0, websitePosts: 0, recentPosts: [] }, message: "Supabase not configured" }));
return;
}
(async () => {
try {
const { count: totalPosts } = await supabase
.from("community_posts")
.select("*", { count: "exact", head: true });
const { count: discordPosts } = await supabase
.from("community_posts")
.select("*", { count: "exact", head: true })
.eq("source", "discord");
const { count: websitePosts } = await supabase
.from("community_posts")
.select("*", { count: "exact", head: true })
.or("source.is.null,source.neq.discord");
const { data: recentPosts } = await supabase
.from("community_posts")
.select("id, content, source, created_at")
.order("created_at", { ascending: false })
.limit(10);
res.writeHead(200);
res.end(
JSON.stringify({
success: true,
stats: {
totalPosts: totalPosts || 0,
discordPosts: discordPosts || 0,
websitePosts: websitePosts || 0,
recentPosts: (recentPosts || []).map(p => ({
id: p.id,
content: p.content?.slice(0, 100) + (p.content?.length > 100 ? "..." : ""),
source: p.source,
created_at: p.created_at,
})),
},
})
);
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ success: false, error: error.message }));
}
})();
return;
}
if (req.url === "/send-to-discord" && req.method === "POST") {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", async () => {
try {
const authHeader = req.headers.authorization;
const expectedToken = process.env.DISCORD_BRIDGE_TOKEN || "aethex-bridge";
if (authHeader !== `Bearer ${expectedToken}`) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized" }));
return;
}
const post = JSON.parse(body);
console.log("[API] Received post to send to Discord:", post.id);
if (sendPostToDiscord) {
const result = await sendPostToDiscord(post, post.author);
res.writeHead(result.success ? 200 : 500);
res.end(JSON.stringify(result));
} else {
res.writeHead(500);
res.end(JSON.stringify({ error: "Feed sync not available" }));
}
} catch (error) {
console.error("[API] Error processing send-to-discord:", error);
res.writeHead(500);
res.end(JSON.stringify({ error: error.message }));
}
});
return;
}
if (req.url === "/bridge-status") {
const channelId = getFeedChannelId();
res.writeHead(200);
res.end(
JSON.stringify({
enabled: !!channelId,
channelId: channelId,
botReady: client.isReady(),
}),
);
return;
}
if (req.url === "/register-commands") {
if (req.method === "GET") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
res.writeHead(200, { "Content-Type": "text/html" });
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>Register Discord Commands</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
text-align: center;
max-width: 500px;
}
h1 { color: #333; margin-bottom: 20px; }
p { color: #666; margin-bottom: 30px; }
button {
background: #667eea;
color: white;
border: none;
padding: 12px 30px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: background 0.3s;
}
button:hover { background: #764ba2; }
button:disabled { background: #ccc; cursor: not-allowed; }
#result { margin-top: 30px; padding: 20px; border-radius: 5px; display: none; }
#result.success { background: #d4edda; color: #155724; display: block; }
#result.error { background: #f8d7da; color: #721c24; display: block; }
#loading { display: none; color: #667eea; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<h1>Discord Commands Registration</h1>
<p>Click to register all ${COMMANDS_TO_REGISTER.length} slash commands</p>
<button id="registerBtn" onclick="registerCommands()">Register Commands</button>
<div id="loading">Registering... please wait...</div>
<div id="result"></div>
</div>
<script>
async function registerCommands() {
const btn = document.getElementById('registerBtn');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
btn.disabled = true;
loading.style.display = 'block';
result.style.display = 'none';
try {
const response = await fetch('/register-commands', {
method: 'POST',
headers: { 'Authorization': 'Bearer ${ADMIN_TOKEN}', 'Content-Type': 'application/json' }
});
const data = await response.json();
loading.style.display = 'none';
if (response.ok && data.success) {
result.className = 'success';
result.innerHTML = '<h3>Success!</h3><p>Registered ' + data.count + ' commands</p>';
} else {
result.className = 'error';
result.innerHTML = '<h3>Error</h3><p>' + (data.error || 'Failed') + '</p>';
}
} catch (error) {
loading.style.display = 'none';
result.className = 'error';
result.innerHTML = '<h3>Error</h3><p>' + error.message + '</p>';
} finally {
btn.disabled = false;
}
}
</script>
</body>
</html>
`);
return;
}
if (req.method === "POST") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
registerDiscordCommands().then((result) => {
if (result.success) {
res.writeHead(200);
res.end(JSON.stringify(result));
} else {
res.writeHead(500);
res.end(JSON.stringify(result));
}
});
return;
}
}
res.writeHead(404);
res.end(JSON.stringify({ error: "Not found" }));
})
.listen(healthPort, () => {
console.log(`Health check server running on port ${healthPort}`);
console.log(`Register commands at: POST http://localhost:${healthPort}/register-commands`);
});
// =============================================================================
// BOT LOGIN AND READY
// =============================================================================
client.login(token).catch((error) => {
console.error("Failed to login:", 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);
});
client.once("ready", () => {
console.log(`Bot logged in as ${client.user.tag}`);
console.log(`Watching ${client.guilds.cache.size} server(s)`);
console.log("Commands are registered via: npm run register-commands");
client.user.setActivity("Protecting the Federation", { type: 3 });
if (setupFeedListener && supabase) {
setupFeedListener(client);
}
sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`);
});
// =============================================================================
// ERROR HANDLING
// =============================================================================
process.on("unhandledRejection", (error) => {
console.error("Unhandled Promise Rejection:", error);
});

View file

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

View file

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

View file

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

View file

@ -9,17 +9,13 @@ module.exports = {
),
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 });
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 {
// Check if user is linked
const { data: link } = await supabase
.from("discord_links")
.select("primary_arm")

View file

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

View file

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

View file

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

View file

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

View file

@ -13,15 +13,10 @@ module.exports = {
.setDescription("Link your Discord account to your AeThex account"),
async execute(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
if (!supabase) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("⚠️ Feature Unavailable")
.setDescription("Account linking is not configured. Contact an administrator.");
return await interaction.editReply({ embeds: [embed] });
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
}
await interaction.deferReply({ ephemeral: true });
try {
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 { createClient } = require("@supabase/supabase-js");
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE,
);
let supabase = null;
if (process.env.SUPABASE_URL && 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
? process.env.DISCORD_MAIN_CHAT_CHANNELS.split(",")[0].trim()
@ -207,6 +210,11 @@ async function checkForNewPosts() {
function setupFeedListener(client) {
discordClient = client;
if (!supabase) {
console.log("[Feed Bridge] No Supabase configured - bridge disabled");
return;
}
if (!FEED_CHANNEL_ID) {
console.log("[Feed Bridge] No DISCORD_MAIN_CHAT_CHANNELS configured - bridge disabled");
return;

View file

@ -7,7 +7,9 @@
"": {
"name": "aethex-unified-bot",
"version": "2.0.0",
"license": "MIT",
"dependencies": {
"@discord/embedded-app-sdk": "^2.4.0",
"@supabase/supabase-js": "^2.38.0",
"axios": "^1.6.0",
"discord.js": "^14.13.0",
@ -20,6 +22,22 @@
"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": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz",
@ -263,6 +281,21 @@
"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": {
"version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
@ -278,6 +311,12 @@
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
"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": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@ -335,6 +374,15 @@
"dev": true,
"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": {
"version": "2.3.0",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -563,6 +617,12 @@
"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": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -827,6 +887,12 @@
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
"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": {
"version": "1.12.1",
"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==",
"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": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
@ -1077,6 +1156,15 @@
"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",
"register-commands": "node scripts/register-commands.js"
},
"keywords": [
"discord",
"bot",
"aethex",
"role-management",
"sentinel",
"security",
"discord.js"
],
"author": "AeThex Team",
"license": "MIT",
"dependencies": {
"@discord/embedded-app-sdk": "^2.4.0",
"@supabase/supabase-js": "^2.38.0",
"axios": "^1.6.0",
"discord.js": "^14.13.0",

View file

@ -1,11 +1,12 @@
# 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
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
- **Federation Sync**: Cross-server role synchronization across 5 realms
- **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
- **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
## Project Structure
```
aethex-bot/
├── bot.js # Main entry point
├── bot.js # Main entry point (merged: original + Sentinel)
├── package.json
├── .env.example
├── .env.example # Complete environment template
├── Dockerfile # Docker deployment config
├── discloud.config # DisCloud hosting config
├── DEPLOYMENT_GUIDE.md # Deployment documentation
├── commands/
│ ├── admin.js # /admin status|heat|servers|threats|federation
│ ├── 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
│ └── 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/
│ └── guildMemberUpdate.js # Federation role sync listener
│ └── messageCreate.js # Message event handler
├── listeners/
│ ├── feedSync.js # Community feed sync
│ └── sentinel/
│ ├── antiNuke.js # Channel delete monitor
│ ├── roleDelete.js # Role delete monitor
@ -42,8 +57,23 @@ aethex-bot/
└── 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 |
|---------|-------------|
| `/admin status` | View bot status and statistics |
@ -60,7 +90,7 @@ aethex-bot/
## 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
- **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
Required:
- `DISCORD_TOKEN` or `DISCORD_BOT_TOKEN` - Bot token
- `DISCORD_CLIENT_ID` - Application ID (currently: 578971245454950421)
### Required
- `DISCORD_BOT_TOKEN` - Bot token from Discord Developer Portal
- `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
- `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
- `ALERT_CHANNEL_ID` - Channel for security alerts
Optional - Supabase:
- `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE` - For user verification features
### Optional - Feed Sync
- `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)
```json
{
"status": "online",
"guilds": 5,
"commands": 4,
"guilds": 8,
"commands": 14,
"uptime": 3600,
"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
cd aethex-bot
npm install
node scripts/register-commands.js # Register slash commands (run once)
npm start
```
Commands are registered automatically on startup or via POST to `/register-commands`.
## Current Status
- Bot is running and connected to 5 servers
- All 4 commands registered (/admin, /federation, /status, /ticket)
- Sentinel listeners active (channel/role delete, ban/kick monitoring)
- Health endpoint available at port 8080
- Bot running as AeThex#9389
- Connected to 8 servers
- 14 commands loaded
- 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