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:
parent
3e4ac076e2
commit
b178664f99
19 changed files with 1285 additions and 199 deletions
4
.replit
4
.replit
|
|
@ -22,6 +22,10 @@ externalPort = 80
|
|||
localPort = 8080
|
||||
externalPort = 8080
|
||||
|
||||
[[ports]]
|
||||
localPort = 38431
|
||||
externalPort = 3000
|
||||
|
||||
[workflows]
|
||||
runButton = "Project"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
211
aethex-bot/DEPLOYMENT_GUIDE.md
Normal file
211
aethex-bot/DEPLOYMENT_GUIDE.md
Normal 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
22
aethex-bot/Dockerfile
Normal 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"]
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
10
aethex-bot/discloud.config
Normal file
10
aethex-bot/discloud.config
Normal 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
88
aethex-bot/package-lock.json
generated
88
aethex-bot/package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
96
replit.md
96
replit.md
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue