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
|
localPort = 8080
|
||||||
externalPort = 8080
|
externalPort = 8080
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 38431
|
||||||
|
externalPort = 3000
|
||||||
|
|
||||||
[workflows]
|
[workflows]
|
||||||
runButton = "Project"
|
runButton = "Project"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,48 @@
|
||||||
# Required
|
# Discord Bot Configuration
|
||||||
DISCORD_BOT_TOKEN=your_discord_bot_token
|
DISCORD_BOT_TOKEN=your_bot_token_here
|
||||||
DISCORD_CLIENT_ID=your_discord_client_id
|
DISCORD_CLIENT_ID=your_client_id_here
|
||||||
|
DISCORD_PUBLIC_KEY=your_public_key_here
|
||||||
|
|
||||||
# Optional - Supabase (for user verification features)
|
# Supabase Configuration (optional - community features require this)
|
||||||
SUPABASE_URL=your_supabase_url
|
SUPABASE_URL=https://your-project.supabase.co
|
||||||
SUPABASE_SERVICE_ROLE=your_supabase_service_role_key
|
SUPABASE_SERVICE_ROLE=your_service_role_key_here
|
||||||
|
|
||||||
# Optional - Federation Guild IDs
|
# API Configuration
|
||||||
|
VITE_API_BASE=https://api.aethex.dev
|
||||||
|
|
||||||
|
# Discord Feed Webhook Configuration
|
||||||
|
DISCORD_FEED_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN
|
||||||
|
DISCORD_FEED_GUILD_ID=515711457946632232
|
||||||
|
DISCORD_FEED_CHANNEL_ID=1425114041021497454
|
||||||
|
|
||||||
|
# Discord Main Chat Channels (comma-separated channel IDs for feed sync)
|
||||||
|
DISCORD_MAIN_CHAT_CHANNELS=channel_id_1,channel_id_2
|
||||||
|
|
||||||
|
# Discord Announcement Channels (comma-separated channel IDs)
|
||||||
|
DISCORD_ANNOUNCEMENT_CHANNELS=1435667453244866702,your_other_channel_ids_here
|
||||||
|
|
||||||
|
# Discord Role Mappings (optional)
|
||||||
|
DISCORD_FOUNDER_ROLE_ID=your_role_id_here
|
||||||
|
DISCORD_ADMIN_ROLE_ID=your_admin_role_id_here
|
||||||
|
|
||||||
|
# Admin API Tokens
|
||||||
|
DISCORD_ADMIN_TOKEN=aethex-bot-admin
|
||||||
|
DISCORD_BRIDGE_TOKEN=aethex-bridge
|
||||||
|
|
||||||
|
# Health Server
|
||||||
|
HEALTH_PORT=8080
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SENTINEL SECURITY CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Federation Guild IDs (optional)
|
||||||
HUB_GUILD_ID=main_hub_server_id
|
HUB_GUILD_ID=main_hub_server_id
|
||||||
LABS_GUILD_ID=labs_server_id
|
LABS_GUILD_ID=labs_server_id
|
||||||
GAMEFORGE_GUILD_ID=gameforge_server_id
|
GAMEFORGE_GUILD_ID=gameforge_server_id
|
||||||
CORP_GUILD_ID=corp_server_id
|
CORP_GUILD_ID=corp_server_id
|
||||||
FOUNDATION_GUILD_ID=foundation_server_id
|
FOUNDATION_GUILD_ID=foundation_server_id
|
||||||
|
|
||||||
# Optional - Security
|
# Security Settings
|
||||||
WHITELISTED_USERS=user_id_1,user_id_2
|
WHITELISTED_USERS=user_id_1,user_id_2
|
||||||
ALERT_CHANNEL_ID=channel_id_for_alerts
|
ALERT_CHANNEL_ID=channel_id_for_alerts
|
||||||
|
|
||||||
# Optional - Health server
|
|
||||||
HEALTH_PORT=8080
|
|
||||||
|
|
|
||||||
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");
|
const path = require("path");
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ENVIRONMENT VALIDATION (Modified: Supabase now optional)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
const token = process.env.DISCORD_BOT_TOKEN;
|
const token = process.env.DISCORD_BOT_TOKEN;
|
||||||
const clientId = process.env.DISCORD_CLIENT_ID;
|
const clientId = process.env.DISCORD_CLIENT_ID;
|
||||||
|
|
||||||
|
|
@ -29,6 +33,10 @@ if (!clientId) {
|
||||||
|
|
||||||
console.log("[Token] Bot token loaded (length: " + token.length + " chars)");
|
console.log("[Token] Bot token loaded (length: " + token.length + " chars)");
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// DISCORD CLIENT SETUP (Modified: Added intents for Sentinel)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
|
|
@ -40,6 +48,10 @@ const client = new Client({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SUPABASE SETUP (Modified: Now optional)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
let supabase = null;
|
let supabase = null;
|
||||||
if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) {
|
if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) {
|
||||||
supabase = createClient(
|
supabase = createClient(
|
||||||
|
|
@ -51,46 +63,9 @@ if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) {
|
||||||
console.log("Supabase not configured - community features will be limited");
|
console.log("Supabase not configured - community features will be limited");
|
||||||
}
|
}
|
||||||
|
|
||||||
client.commands = new Collection();
|
// =============================================================================
|
||||||
|
// SENTINEL: HEAT TRACKING SYSTEM (New)
|
||||||
const commandsPath = path.join(__dirname, "commands");
|
// =============================================================================
|
||||||
if (fs.existsSync(commandsPath)) {
|
|
||||||
const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js"));
|
|
||||||
for (const file of commandFiles) {
|
|
||||||
const filePath = path.join(commandsPath, file);
|
|
||||||
const command = require(filePath);
|
|
||||||
if ("data" in command && "execute" in command) {
|
|
||||||
client.commands.set(command.data.name, command);
|
|
||||||
console.log(`Loaded command: ${command.data.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventsPath = path.join(__dirname, "events");
|
|
||||||
if (fs.existsSync(eventsPath)) {
|
|
||||||
const eventFiles = fs.readdirSync(eventsPath).filter((file) => file.endsWith(".js"));
|
|
||||||
for (const file of eventFiles) {
|
|
||||||
const filePath = path.join(eventsPath, file);
|
|
||||||
const event = require(filePath);
|
|
||||||
if ("name" in event && "execute" in event) {
|
|
||||||
client.on(event.name, (...args) => event.execute(...args, client, supabase));
|
|
||||||
console.log(`Loaded event: ${event.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sentinelPath = path.join(__dirname, "listeners", "sentinel");
|
|
||||||
if (fs.existsSync(sentinelPath)) {
|
|
||||||
const sentinelFiles = fs.readdirSync(sentinelPath).filter((file) => file.endsWith(".js"));
|
|
||||||
for (const file of sentinelFiles) {
|
|
||||||
const filePath = path.join(sentinelPath, file);
|
|
||||||
const listener = require(filePath);
|
|
||||||
if ("name" in listener && "execute" in listener) {
|
|
||||||
client.on(listener.name, (...args) => listener.execute(...args, client));
|
|
||||||
console.log(`Loaded sentinel listener: ${listener.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const heatMap = new Map();
|
const heatMap = new Map();
|
||||||
const HEAT_THRESHOLD = 3;
|
const HEAT_THRESHOLD = 3;
|
||||||
|
|
@ -125,6 +100,10 @@ client.addHeat = addHeat;
|
||||||
client.getHeat = getHeat;
|
client.getHeat = getHeat;
|
||||||
client.HEAT_THRESHOLD = HEAT_THRESHOLD;
|
client.HEAT_THRESHOLD = HEAT_THRESHOLD;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SENTINEL: FEDERATION MAPPINGS (New)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
const federationMappings = new Map();
|
const federationMappings = new Map();
|
||||||
client.federationMappings = federationMappings;
|
client.federationMappings = federationMappings;
|
||||||
|
|
||||||
|
|
@ -137,9 +116,17 @@ const REALM_GUILDS = {
|
||||||
};
|
};
|
||||||
client.REALM_GUILDS = REALM_GUILDS;
|
client.REALM_GUILDS = REALM_GUILDS;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SENTINEL: TICKET TRACKING (New)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
const activeTickets = new Map();
|
const activeTickets = new Map();
|
||||||
client.activeTickets = activeTickets;
|
client.activeTickets = activeTickets;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SENTINEL: ALERT SYSTEM (New)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
let alertChannelId = process.env.ALERT_CHANNEL_ID;
|
let alertChannelId = process.env.ALERT_CHANNEL_ID;
|
||||||
client.alertChannelId = alertChannelId;
|
client.alertChannelId = alertChannelId;
|
||||||
|
|
||||||
|
|
@ -160,30 +147,88 @@ async function sendAlert(message, embed = null) {
|
||||||
}
|
}
|
||||||
client.sendAlert = sendAlert;
|
client.sendAlert = sendAlert;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// COMMAND LOADING
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
client.commands = new Collection();
|
||||||
|
|
||||||
|
const commandsPath = path.join(__dirname, "commands");
|
||||||
|
if (fs.existsSync(commandsPath)) {
|
||||||
|
const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js"));
|
||||||
|
for (const file of commandFiles) {
|
||||||
|
const filePath = path.join(commandsPath, file);
|
||||||
|
const command = require(filePath);
|
||||||
|
if ("data" in command && "execute" in command) {
|
||||||
|
client.commands.set(command.data.name, command);
|
||||||
|
console.log(`Loaded command: ${command.data.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// EVENT LOADING
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const eventsPath = path.join(__dirname, "events");
|
||||||
|
if (fs.existsSync(eventsPath)) {
|
||||||
|
const eventFiles = fs.readdirSync(eventsPath).filter((file) => file.endsWith(".js"));
|
||||||
|
for (const file of eventFiles) {
|
||||||
|
const filePath = path.join(eventsPath, file);
|
||||||
|
const event = require(filePath);
|
||||||
|
if ("name" in event && "execute" in event) {
|
||||||
|
client.on(event.name, (...args) => event.execute(...args, client, supabase));
|
||||||
|
console.log(`Loaded event: ${event.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SENTINEL LISTENER LOADING (New)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const sentinelPath = path.join(__dirname, "listeners", "sentinel");
|
||||||
|
if (fs.existsSync(sentinelPath)) {
|
||||||
|
const sentinelFiles = fs.readdirSync(sentinelPath).filter((file) => file.endsWith(".js"));
|
||||||
|
for (const file of sentinelFiles) {
|
||||||
|
const filePath = path.join(sentinelPath, file);
|
||||||
|
const listener = require(filePath);
|
||||||
|
if ("name" in listener && "execute" in listener) {
|
||||||
|
client.on(listener.name, (...args) => listener.execute(...args, client));
|
||||||
|
console.log(`Loaded sentinel listener: ${listener.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// FEED SYNC SETUP (Modified: Guard for missing Supabase)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
let feedSyncModule = null;
|
let feedSyncModule = null;
|
||||||
|
let setupFeedListener = null;
|
||||||
|
let sendPostToDiscord = null;
|
||||||
|
let getFeedChannelId = () => null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
feedSyncModule = require("./listeners/feedSync");
|
feedSyncModule = require("./listeners/feedSync");
|
||||||
|
setupFeedListener = feedSyncModule.setupFeedListener;
|
||||||
|
sendPostToDiscord = feedSyncModule.sendPostToDiscord;
|
||||||
|
getFeedChannelId = feedSyncModule.getFeedChannelId;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Feed sync module not available");
|
console.log("Feed sync module not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
client.once("ready", () => {
|
// =============================================================================
|
||||||
console.log(`Bot logged in as ${client.user.tag}`);
|
// INTERACTION HANDLER (Modified: Added button handling for tickets)
|
||||||
console.log(`Watching ${client.guilds.cache.size} server(s)`);
|
// =============================================================================
|
||||||
|
|
||||||
client.user.setActivity("Protecting the Federation", { type: 3 });
|
|
||||||
|
|
||||||
if (feedSyncModule && feedSyncModule.setupFeedListener && supabase) {
|
|
||||||
feedSyncModule.setupFeedListener(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`);
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("interactionCreate", async (interaction) => {
|
client.on("interactionCreate", async (interaction) => {
|
||||||
if (interaction.isChatInputCommand()) {
|
if (interaction.isChatInputCommand()) {
|
||||||
const command = client.commands.get(interaction.commandName);
|
const command = client.commands.get(interaction.commandName);
|
||||||
if (!command) return;
|
if (!command) {
|
||||||
|
console.warn(`No command matching ${interaction.commandName} was found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await command.execute(interaction, supabase, client);
|
await command.execute(interaction, supabase, client);
|
||||||
|
|
@ -192,7 +237,8 @@ client.on("interactionCreate", async (interaction) => {
|
||||||
const errorEmbed = new EmbedBuilder()
|
const errorEmbed = new EmbedBuilder()
|
||||||
.setColor(0xff0000)
|
.setColor(0xff0000)
|
||||||
.setTitle("Command Error")
|
.setTitle("Command Error")
|
||||||
.setDescription("There was an error while executing this command.");
|
.setDescription("There was an error while executing this command.")
|
||||||
|
.setFooter({ text: "Contact support if this persists" });
|
||||||
|
|
||||||
if (interaction.replied || interaction.deferred) {
|
if (interaction.replied || interaction.deferred) {
|
||||||
await interaction.followUp({ embeds: [errorEmbed], ephemeral: true });
|
await interaction.followUp({ embeds: [errorEmbed], ephemeral: true });
|
||||||
|
|
@ -222,54 +268,710 @@ client.on("interactionCreate", async (interaction) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// COMMANDS FOR REGISTRATION (Modified: Added Sentinel commands)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const COMMANDS_TO_REGISTER = [
|
||||||
|
{
|
||||||
|
name: "verify",
|
||||||
|
description: "Link your Discord account to AeThex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set-realm",
|
||||||
|
description: "Choose your primary arm/realm (Labs, GameForge, Corp, etc.)",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "realm",
|
||||||
|
type: 3,
|
||||||
|
description: "Your primary realm",
|
||||||
|
required: true,
|
||||||
|
choices: [
|
||||||
|
{ name: "Labs", value: "labs" },
|
||||||
|
{ name: "GameForge", value: "gameforge" },
|
||||||
|
{ name: "Corp", value: "corp" },
|
||||||
|
{ name: "Foundation", value: "foundation" },
|
||||||
|
{ name: "Dev-Link", value: "devlink" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "profile",
|
||||||
|
description: "View your linked AeThex profile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unlink",
|
||||||
|
description: "Disconnect your Discord account from AeThex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "verify-role",
|
||||||
|
description: "Check your assigned Discord roles",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "help",
|
||||||
|
description: "View all AeThex bot commands and features",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stats",
|
||||||
|
description: "View your AeThex statistics and activity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "leaderboard",
|
||||||
|
description: "View the top AeThex contributors",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "category",
|
||||||
|
type: 3,
|
||||||
|
description: "Leaderboard category",
|
||||||
|
required: false,
|
||||||
|
choices: [
|
||||||
|
{ name: "Most Active (Posts)", value: "posts" },
|
||||||
|
{ name: "Most Liked", value: "likes" },
|
||||||
|
{ name: "Top Creators", value: "creators" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "post",
|
||||||
|
description: "Create a post in the AeThex community feed",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: 3,
|
||||||
|
description: "Your post content",
|
||||||
|
required: true,
|
||||||
|
max_length: 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "category",
|
||||||
|
type: 3,
|
||||||
|
description: "Post category",
|
||||||
|
required: false,
|
||||||
|
choices: [
|
||||||
|
{ name: "General", value: "general" },
|
||||||
|
{ name: "Project Update", value: "project_update" },
|
||||||
|
{ name: "Question", value: "question" },
|
||||||
|
{ name: "Idea", value: "idea" },
|
||||||
|
{ name: "Announcement", value: "announcement" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image",
|
||||||
|
type: 11,
|
||||||
|
description: "Attach an image to your post",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "refresh-roles",
|
||||||
|
description: "Refresh your Discord roles based on your AeThex profile",
|
||||||
|
},
|
||||||
|
// Sentinel Commands
|
||||||
|
{
|
||||||
|
name: "admin",
|
||||||
|
description: "Admin controls for bot management",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "action",
|
||||||
|
type: 3,
|
||||||
|
description: "Admin action to perform",
|
||||||
|
required: true,
|
||||||
|
choices: [
|
||||||
|
{ name: "Status", value: "status" },
|
||||||
|
{ name: "Heat Check", value: "heat" },
|
||||||
|
{ name: "Servers", value: "servers" },
|
||||||
|
{ name: "Threats", value: "threats" },
|
||||||
|
{ name: "Federation", value: "federation" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user",
|
||||||
|
type: 6,
|
||||||
|
description: "Target user (for heat check)",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "federation",
|
||||||
|
description: "Manage federation role sync",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "action",
|
||||||
|
type: 3,
|
||||||
|
description: "Federation action",
|
||||||
|
required: true,
|
||||||
|
choices: [
|
||||||
|
{ name: "Link Role", value: "link" },
|
||||||
|
{ name: "Unlink Role", value: "unlink" },
|
||||||
|
{ name: "List Linked", value: "list" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "role",
|
||||||
|
type: 8,
|
||||||
|
description: "Role to link/unlink",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status",
|
||||||
|
description: "View network status and bot information",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ticket",
|
||||||
|
description: "Create or close support tickets",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "action",
|
||||||
|
type: 3,
|
||||||
|
description: "Ticket action",
|
||||||
|
required: true,
|
||||||
|
choices: [
|
||||||
|
{ name: "Create", value: "create" },
|
||||||
|
{ name: "Close", value: "close" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reason",
|
||||||
|
type: 3,
|
||||||
|
description: "Reason for ticket (when creating)",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// COMMAND REGISTRATION FUNCTION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
async function registerDiscordCommands() {
|
||||||
|
try {
|
||||||
|
const rest = new REST({ version: "10" }).setToken(
|
||||||
|
process.env.DISCORD_BOT_TOKEN,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Registering ${COMMANDS_TO_REGISTER.length} slash commands...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await rest.put(
|
||||||
|
Routes.applicationCommands(process.env.DISCORD_CLIENT_ID),
|
||||||
|
{ body: COMMANDS_TO_REGISTER },
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Successfully registered ${data.length} slash commands`);
|
||||||
|
return { success: true, count: data.length, results: null };
|
||||||
|
} catch (bulkError) {
|
||||||
|
if (bulkError.code === 50240) {
|
||||||
|
console.warn(
|
||||||
|
"Error 50240: Entry Point detected. Registering individually...",
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
let successCount = 0;
|
||||||
|
let skipCount = 0;
|
||||||
|
|
||||||
|
for (const command of COMMANDS_TO_REGISTER) {
|
||||||
|
try {
|
||||||
|
const posted = await rest.post(
|
||||||
|
Routes.applicationCommands(process.env.DISCORD_CLIENT_ID),
|
||||||
|
{ body: command },
|
||||||
|
);
|
||||||
|
results.push({
|
||||||
|
name: command.name,
|
||||||
|
status: "registered",
|
||||||
|
id: posted.id,
|
||||||
|
});
|
||||||
|
successCount++;
|
||||||
|
} catch (postError) {
|
||||||
|
if (postError.code === 50045) {
|
||||||
|
results.push({
|
||||||
|
name: command.name,
|
||||||
|
status: "already_exists",
|
||||||
|
});
|
||||||
|
skipCount++;
|
||||||
|
} else {
|
||||||
|
results.push({
|
||||||
|
name: command.name,
|
||||||
|
status: "error",
|
||||||
|
error: postError.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Registration complete: ${successCount} new, ${skipCount} already existed`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
count: successCount,
|
||||||
|
skipped: skipCount,
|
||||||
|
results,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw bulkError;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to register commands:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HTTP SERVER (Modified: Added Sentinel stats to health endpoint)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
const healthPort = process.env.HEALTH_PORT || 8080;
|
const healthPort = process.env.HEALTH_PORT || 8080;
|
||||||
|
const ADMIN_TOKEN = process.env.DISCORD_ADMIN_TOKEN || "aethex-bot-admin";
|
||||||
|
|
||||||
http.createServer((req, res) => {
|
const checkAdminAuth = (req) => {
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
const authHeader = req.headers.authorization;
|
||||||
res.setHeader("Content-Type", "application/json");
|
return authHeader === `Bearer ${ADMIN_TOKEN}`;
|
||||||
|
};
|
||||||
|
|
||||||
if (req.url === "/health") {
|
http
|
||||||
res.writeHead(200);
|
.createServer((req, res) => {
|
||||||
res.end(JSON.stringify({
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
status: "online",
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||||
guilds: client.guilds.cache.size,
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||||
commands: client.commands.size,
|
res.setHeader("Content-Type", "application/json");
|
||||||
uptime: Math.floor(process.uptime()),
|
|
||||||
heatMapSize: heatMap.size,
|
|
||||||
supabaseConnected: !!supabase,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.url === "/stats") {
|
if (req.method === "OPTIONS") {
|
||||||
const guildStats = client.guilds.cache.map(g => ({
|
res.writeHead(200);
|
||||||
id: g.id,
|
res.end();
|
||||||
name: g.name,
|
return;
|
||||||
memberCount: g.memberCount,
|
}
|
||||||
}));
|
|
||||||
res.writeHead(200);
|
|
||||||
res.end(JSON.stringify({
|
|
||||||
guilds: guildStats,
|
|
||||||
totalMembers: guildStats.reduce((sum, g) => sum + g.memberCount, 0),
|
|
||||||
uptime: Math.floor(process.uptime()),
|
|
||||||
activeTickets: activeTickets.size,
|
|
||||||
heatEvents: heatMap.size,
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.writeHead(404);
|
if (req.url === "/health") {
|
||||||
res.end(JSON.stringify({ error: "Not found" }));
|
res.writeHead(200);
|
||||||
}).listen(healthPort, () => {
|
res.end(
|
||||||
console.log(`Health server running on port ${healthPort}`);
|
JSON.stringify({
|
||||||
});
|
status: "online",
|
||||||
|
guilds: client.guilds.cache.size,
|
||||||
|
commands: client.commands.size,
|
||||||
|
uptime: Math.floor(process.uptime()),
|
||||||
|
heatMapSize: heatMap.size,
|
||||||
|
supabaseConnected: !!supabase,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === "/stats") {
|
||||||
|
const guildStats = client.guilds.cache.map(g => ({
|
||||||
|
id: g.id,
|
||||||
|
name: g.name,
|
||||||
|
memberCount: g.memberCount,
|
||||||
|
}));
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
guilds: guildStats,
|
||||||
|
totalMembers: guildStats.reduce((sum, g) => sum + g.memberCount, 0),
|
||||||
|
uptime: Math.floor(process.uptime()),
|
||||||
|
activeTickets: activeTickets.size,
|
||||||
|
heatEvents: heatMap.size,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === "/bot-status") {
|
||||||
|
if (!checkAdminAuth(req)) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelId = getFeedChannelId();
|
||||||
|
const guilds = client.guilds.cache.map((guild) => ({
|
||||||
|
id: guild.id,
|
||||||
|
name: guild.name,
|
||||||
|
memberCount: guild.memberCount,
|
||||||
|
icon: guild.iconURL(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
status: client.isReady() ? "online" : "offline",
|
||||||
|
bot: {
|
||||||
|
tag: client.user?.tag || "Not logged in",
|
||||||
|
id: client.user?.id,
|
||||||
|
avatar: client.user?.displayAvatarURL(),
|
||||||
|
},
|
||||||
|
guilds: guilds,
|
||||||
|
guildCount: client.guilds.cache.size,
|
||||||
|
commands: Array.from(client.commands.keys()),
|
||||||
|
commandCount: client.commands.size,
|
||||||
|
uptime: Math.floor(process.uptime()),
|
||||||
|
feedBridge: {
|
||||||
|
enabled: !!channelId,
|
||||||
|
channelId: channelId,
|
||||||
|
},
|
||||||
|
sentinel: {
|
||||||
|
heatMapSize: heatMap.size,
|
||||||
|
activeTickets: activeTickets.size,
|
||||||
|
federationMappings: federationMappings.size,
|
||||||
|
},
|
||||||
|
supabaseConnected: !!supabase,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === "/linked-users") {
|
||||||
|
if (!checkAdminAuth(req)) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supabase) {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify({ success: true, links: [], count: 0, message: "Supabase not configured" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { data: links, error } = await supabase
|
||||||
|
.from("discord_links")
|
||||||
|
.select("discord_id, user_id, primary_arm, created_at")
|
||||||
|
.order("created_at", { ascending: false })
|
||||||
|
.limit(50);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
const enrichedLinks = await Promise.all(
|
||||||
|
(links || []).map(async (link) => {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from("user_profiles")
|
||||||
|
.select("username, avatar_url")
|
||||||
|
.eq("id", link.user_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return {
|
||||||
|
discord_id: link.discord_id.slice(0, 6) + "***",
|
||||||
|
user_id: link.user_id.slice(0, 8) + "...",
|
||||||
|
primary_arm: link.primary_arm,
|
||||||
|
created_at: link.created_at,
|
||||||
|
profile: profile ? {
|
||||||
|
username: profile.username,
|
||||||
|
avatar_url: profile.avatar_url,
|
||||||
|
} : null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify({ success: true, links: enrichedLinks, count: enrichedLinks.length }));
|
||||||
|
} catch (error) {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify({ success: false, error: error.message }));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === "/command-stats") {
|
||||||
|
if (!checkAdminAuth(req)) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
commands: COMMANDS_TO_REGISTER.map((cmd) => ({
|
||||||
|
name: cmd.name,
|
||||||
|
description: cmd.description,
|
||||||
|
options: cmd.options?.length || 0,
|
||||||
|
})),
|
||||||
|
totalCommands: COMMANDS_TO_REGISTER.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify({ success: true, stats }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === "/feed-stats") {
|
||||||
|
if (!checkAdminAuth(req)) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supabase) {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify({ success: true, stats: { totalPosts: 0, discordPosts: 0, websitePosts: 0, recentPosts: [] }, message: "Supabase not configured" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { count: totalPosts } = await supabase
|
||||||
|
.from("community_posts")
|
||||||
|
.select("*", { count: "exact", head: true });
|
||||||
|
|
||||||
|
const { count: discordPosts } = await supabase
|
||||||
|
.from("community_posts")
|
||||||
|
.select("*", { count: "exact", head: true })
|
||||||
|
.eq("source", "discord");
|
||||||
|
|
||||||
|
const { count: websitePosts } = await supabase
|
||||||
|
.from("community_posts")
|
||||||
|
.select("*", { count: "exact", head: true })
|
||||||
|
.or("source.is.null,source.neq.discord");
|
||||||
|
|
||||||
|
const { data: recentPosts } = await supabase
|
||||||
|
.from("community_posts")
|
||||||
|
.select("id, content, source, created_at")
|
||||||
|
.order("created_at", { ascending: false })
|
||||||
|
.limit(10);
|
||||||
|
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
stats: {
|
||||||
|
totalPosts: totalPosts || 0,
|
||||||
|
discordPosts: discordPosts || 0,
|
||||||
|
websitePosts: websitePosts || 0,
|
||||||
|
recentPosts: (recentPosts || []).map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
content: p.content?.slice(0, 100) + (p.content?.length > 100 ? "..." : ""),
|
||||||
|
source: p.source,
|
||||||
|
created_at: p.created_at,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify({ success: false, error: error.message }));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === "/send-to-discord" && req.method === "POST") {
|
||||||
|
let body = "";
|
||||||
|
req.on("data", (chunk) => {
|
||||||
|
body += chunk.toString();
|
||||||
|
});
|
||||||
|
req.on("end", async () => {
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
const expectedToken = process.env.DISCORD_BRIDGE_TOKEN || "aethex-bridge";
|
||||||
|
if (authHeader !== `Bearer ${expectedToken}`) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = JSON.parse(body);
|
||||||
|
console.log("[API] Received post to send to Discord:", post.id);
|
||||||
|
|
||||||
|
if (sendPostToDiscord) {
|
||||||
|
const result = await sendPostToDiscord(post, post.author);
|
||||||
|
res.writeHead(result.success ? 200 : 500);
|
||||||
|
res.end(JSON.stringify(result));
|
||||||
|
} else {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify({ error: "Feed sync not available" }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[API] Error processing send-to-discord:", error);
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify({ error: error.message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === "/bridge-status") {
|
||||||
|
const channelId = getFeedChannelId();
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
enabled: !!channelId,
|
||||||
|
channelId: channelId,
|
||||||
|
botReady: client.isReady(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === "/register-commands") {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
if (!checkAdminAuth(req)) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { "Content-Type": "text/html" });
|
||||||
|
res.end(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Register Discord Commands</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
h1 { color: #333; margin-bottom: 20px; }
|
||||||
|
p { color: #666; margin-bottom: 30px; }
|
||||||
|
button {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 30px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
button:hover { background: #764ba2; }
|
||||||
|
button:disabled { background: #ccc; cursor: not-allowed; }
|
||||||
|
#result { margin-top: 30px; padding: 20px; border-radius: 5px; display: none; }
|
||||||
|
#result.success { background: #d4edda; color: #155724; display: block; }
|
||||||
|
#result.error { background: #f8d7da; color: #721c24; display: block; }
|
||||||
|
#loading { display: none; color: #667eea; font-weight: bold; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Discord Commands Registration</h1>
|
||||||
|
<p>Click to register all ${COMMANDS_TO_REGISTER.length} slash commands</p>
|
||||||
|
<button id="registerBtn" onclick="registerCommands()">Register Commands</button>
|
||||||
|
<div id="loading">Registering... please wait...</div>
|
||||||
|
<div id="result"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function registerCommands() {
|
||||||
|
const btn = document.getElementById('registerBtn');
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
const result = document.getElementById('result');
|
||||||
|
btn.disabled = true;
|
||||||
|
loading.style.display = 'block';
|
||||||
|
result.style.display = 'none';
|
||||||
|
try {
|
||||||
|
const response = await fetch('/register-commands', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': 'Bearer ${ADMIN_TOKEN}', 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
loading.style.display = 'none';
|
||||||
|
if (response.ok && data.success) {
|
||||||
|
result.className = 'success';
|
||||||
|
result.innerHTML = '<h3>Success!</h3><p>Registered ' + data.count + ' commands</p>';
|
||||||
|
} else {
|
||||||
|
result.className = 'error';
|
||||||
|
result.innerHTML = '<h3>Error</h3><p>' + (data.error || 'Failed') + '</p>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
loading.style.display = 'none';
|
||||||
|
result.className = 'error';
|
||||||
|
result.innerHTML = '<h3>Error</h3><p>' + error.message + '</p>';
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST") {
|
||||||
|
if (!checkAdminAuth(req)) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerDiscordCommands().then((result) => {
|
||||||
|
if (result.success) {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify(result));
|
||||||
|
} else {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify(result));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end(JSON.stringify({ error: "Not found" }));
|
||||||
|
})
|
||||||
|
.listen(healthPort, () => {
|
||||||
|
console.log(`Health check server running on port ${healthPort}`);
|
||||||
|
console.log(`Register commands at: POST http://localhost:${healthPort}/register-commands`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// BOT LOGIN AND READY
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
client.login(token).catch((error) => {
|
client.login(token).catch((error) => {
|
||||||
console.error("Failed to login:", error.message);
|
console.error("Failed to login to Discord");
|
||||||
|
console.error(`Error Code: ${error.code}`);
|
||||||
|
console.error(`Error Message: ${error.message}`);
|
||||||
|
|
||||||
|
if (error.code === "TokenInvalid") {
|
||||||
|
console.error("\nDISCORD_BOT_TOKEN is invalid!");
|
||||||
|
console.error("Get a new token from: https://discord.com/developers/applications");
|
||||||
|
}
|
||||||
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
client.once("ready", () => {
|
||||||
|
console.log(`Bot logged in as ${client.user.tag}`);
|
||||||
|
console.log(`Watching ${client.guilds.cache.size} server(s)`);
|
||||||
|
console.log("Commands are registered via: npm run register-commands");
|
||||||
|
|
||||||
|
client.user.setActivity("Protecting the Federation", { type: 3 });
|
||||||
|
|
||||||
|
if (setupFeedListener && supabase) {
|
||||||
|
setupFeedListener(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ERROR HANDLING
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
process.on("unhandledRejection", (error) => {
|
process.on("unhandledRejection", (error) => {
|
||||||
console.error("Unhandled Promise Rejection:", error);
|
console.error("Unhandled Promise Rejection:", error);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,10 @@ module.exports = {
|
||||||
),
|
),
|
||||||
|
|
||||||
async execute(interaction, supabase) {
|
async execute(interaction, supabase) {
|
||||||
await interaction.deferReply();
|
|
||||||
|
|
||||||
if (!supabase) {
|
if (!supabase) {
|
||||||
const embed = new EmbedBuilder()
|
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
|
||||||
.setColor(0xff6b6b)
|
|
||||||
.setTitle("⚠️ Feature Unavailable")
|
|
||||||
.setDescription("Leaderboard is not configured. Contact an administrator.");
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
|
||||||
}
|
}
|
||||||
|
await interaction.deferReply();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const category = interaction.options.getString("category") || "posts";
|
const category = interaction.options.getString("category") || "posts";
|
||||||
|
|
|
||||||
|
|
@ -39,15 +39,10 @@ module.exports = {
|
||||||
),
|
),
|
||||||
|
|
||||||
async execute(interaction, supabase, client) {
|
async execute(interaction, supabase, client) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
|
|
||||||
if (!supabase) {
|
if (!supabase) {
|
||||||
const embed = new EmbedBuilder()
|
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
|
||||||
.setColor(0xff6b6b)
|
|
||||||
.setTitle("⚠️ Feature Unavailable")
|
|
||||||
.setDescription("Posting is not configured. Contact an administrator.");
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
|
||||||
}
|
}
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: link } = await supabase
|
const { data: link } = await supabase
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,10 @@ module.exports = {
|
||||||
.setDescription("View your AeThex profile in Discord"),
|
.setDescription("View your AeThex profile in Discord"),
|
||||||
|
|
||||||
async execute(interaction, supabase) {
|
async execute(interaction, supabase) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
|
|
||||||
if (!supabase) {
|
if (!supabase) {
|
||||||
const embed = new EmbedBuilder()
|
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
|
||||||
.setColor(0xff6b6b)
|
|
||||||
.setTitle("⚠️ Feature Unavailable")
|
|
||||||
.setDescription("Profile features are not configured. Contact an administrator.");
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
|
||||||
}
|
}
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: link } = await supabase
|
const { data: link } = await supabase
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,13 @@ module.exports = {
|
||||||
),
|
),
|
||||||
|
|
||||||
async execute(interaction, supabase, client) {
|
async execute(interaction, supabase, client) {
|
||||||
|
if (!supabase) {
|
||||||
|
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
|
||||||
|
}
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
if (!supabase) {
|
|
||||||
const embed = new EmbedBuilder()
|
|
||||||
.setColor(0xff6b6b)
|
|
||||||
.setTitle("⚠️ Feature Unavailable")
|
|
||||||
.setDescription("Role sync is not configured. Contact an administrator.");
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if user is linked
|
||||||
const { data: link } = await supabase
|
const { data: link } = await supabase
|
||||||
.from("discord_links")
|
.from("discord_links")
|
||||||
.select("primary_arm")
|
.select("primary_arm")
|
||||||
|
|
|
||||||
|
|
@ -32,15 +32,10 @@ module.exports = {
|
||||||
.setDescription("Set your primary AeThex realm/arm"),
|
.setDescription("Set your primary AeThex realm/arm"),
|
||||||
|
|
||||||
async execute(interaction, supabase, client) {
|
async execute(interaction, supabase, client) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
|
|
||||||
if (!supabase) {
|
if (!supabase) {
|
||||||
const embed = new EmbedBuilder()
|
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
|
||||||
.setColor(0xff6b6b)
|
|
||||||
.setTitle("⚠️ Feature Unavailable")
|
|
||||||
.setDescription("Realm settings are not configured. Contact an administrator.");
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
|
||||||
}
|
}
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: link } = await supabase
|
const { data: link } = await supabase
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,10 @@ module.exports = {
|
||||||
.setDescription("View your AeThex statistics and activity"),
|
.setDescription("View your AeThex statistics and activity"),
|
||||||
|
|
||||||
async execute(interaction, supabase) {
|
async execute(interaction, supabase) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
|
|
||||||
if (!supabase) {
|
if (!supabase) {
|
||||||
const embed = new EmbedBuilder()
|
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
|
||||||
.setColor(0xff6b6b)
|
|
||||||
.setTitle("⚠️ Feature Unavailable")
|
|
||||||
.setDescription("Stats are not configured. Contact an administrator.");
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
|
||||||
}
|
}
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: link } = await supabase
|
const { data: link } = await supabase
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,10 @@ module.exports = {
|
||||||
.setDescription("Unlink your Discord account from AeThex"),
|
.setDescription("Unlink your Discord account from AeThex"),
|
||||||
|
|
||||||
async execute(interaction, supabase) {
|
async execute(interaction, supabase) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
|
|
||||||
if (!supabase) {
|
if (!supabase) {
|
||||||
const embed = new EmbedBuilder()
|
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
|
||||||
.setColor(0xff6b6b)
|
|
||||||
.setTitle("⚠️ Feature Unavailable")
|
|
||||||
.setDescription("Account unlinking is not configured. Contact an administrator.");
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
|
||||||
}
|
}
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: link } = await supabase
|
const { data: link } = await supabase
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,10 @@ module.exports = {
|
||||||
.setDescription("Check your AeThex-assigned Discord roles"),
|
.setDescription("Check your AeThex-assigned Discord roles"),
|
||||||
|
|
||||||
async execute(interaction, supabase) {
|
async execute(interaction, supabase) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
|
|
||||||
if (!supabase) {
|
if (!supabase) {
|
||||||
const embed = new EmbedBuilder()
|
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
|
||||||
.setColor(0xff6b6b)
|
|
||||||
.setTitle("⚠️ Feature Unavailable")
|
|
||||||
.setDescription("Role verification is not configured. Contact an administrator.");
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
|
||||||
}
|
}
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: link } = await supabase
|
const { data: link } = await supabase
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,10 @@ module.exports = {
|
||||||
.setDescription("Link your Discord account to your AeThex account"),
|
.setDescription("Link your Discord account to your AeThex account"),
|
||||||
|
|
||||||
async execute(interaction, supabase, client) {
|
async execute(interaction, supabase, client) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
|
|
||||||
if (!supabase) {
|
if (!supabase) {
|
||||||
const embed = new EmbedBuilder()
|
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
|
||||||
.setColor(0xff6b6b)
|
|
||||||
.setTitle("⚠️ Feature Unavailable")
|
|
||||||
.setDescription("Account linking is not configured. Contact an administrator.");
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
|
||||||
}
|
}
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: existingLink } = await supabase
|
const { data: existingLink } = await supabase
|
||||||
|
|
|
||||||
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 { EmbedBuilder } = require("discord.js");
|
||||||
const { createClient } = require("@supabase/supabase-js");
|
const { createClient } = require("@supabase/supabase-js");
|
||||||
|
|
||||||
const supabase = createClient(
|
let supabase = null;
|
||||||
process.env.SUPABASE_URL,
|
if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) {
|
||||||
process.env.SUPABASE_SERVICE_ROLE,
|
supabase = createClient(
|
||||||
);
|
process.env.SUPABASE_URL,
|
||||||
|
process.env.SUPABASE_SERVICE_ROLE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const FEED_CHANNEL_ID = process.env.DISCORD_MAIN_CHAT_CHANNELS
|
const FEED_CHANNEL_ID = process.env.DISCORD_MAIN_CHAT_CHANNELS
|
||||||
? process.env.DISCORD_MAIN_CHAT_CHANNELS.split(",")[0].trim()
|
? process.env.DISCORD_MAIN_CHAT_CHANNELS.split(",")[0].trim()
|
||||||
|
|
@ -207,6 +210,11 @@ async function checkForNewPosts() {
|
||||||
function setupFeedListener(client) {
|
function setupFeedListener(client) {
|
||||||
discordClient = client;
|
discordClient = client;
|
||||||
|
|
||||||
|
if (!supabase) {
|
||||||
|
console.log("[Feed Bridge] No Supabase configured - bridge disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!FEED_CHANNEL_ID) {
|
if (!FEED_CHANNEL_ID) {
|
||||||
console.log("[Feed Bridge] No DISCORD_MAIN_CHAT_CHANNELS configured - bridge disabled");
|
console.log("[Feed Bridge] No DISCORD_MAIN_CHAT_CHANNELS configured - bridge disabled");
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
88
aethex-bot/package-lock.json
generated
88
aethex-bot/package-lock.json
generated
|
|
@ -7,7 +7,9 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "aethex-unified-bot",
|
"name": "aethex-unified-bot",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@discord/embedded-app-sdk": "^2.4.0",
|
||||||
"@supabase/supabase-js": "^2.38.0",
|
"@supabase/supabase-js": "^2.38.0",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"discord.js": "^14.13.0",
|
"discord.js": "^14.13.0",
|
||||||
|
|
@ -20,6 +22,22 @@
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@discord/embedded-app-sdk": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discord/embedded-app-sdk/-/embedded-app-sdk-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-kIIS79tuVKvu9YC6GIuvBSfUqNa6511UqafD4i3qGjWSRqVulioYuRzZ+M9D9/KZ2wuu0nQ5IWIYlnh1bsy2tg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash.transform": "^4.6.6",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
|
"big-integer": "^1.6.48",
|
||||||
|
"decimal.js-light": "^2.5.0",
|
||||||
|
"eventemitter3": "^5.0.0",
|
||||||
|
"lodash.transform": "^4.6.0",
|
||||||
|
"uuid": "^11.0.0",
|
||||||
|
"zod": "^3.9.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@discordjs/builders": {
|
"node_modules/@discordjs/builders": {
|
||||||
"version": "1.13.1",
|
"version": "1.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz",
|
||||||
|
|
@ -263,6 +281,21 @@
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/lodash.transform": {
|
||||||
|
"version": "4.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash.transform/-/lodash.transform-4.6.9.tgz",
|
||||||
|
"integrity": "sha512-1iIn+l7Vrj8hsr2iZLtxRkcV9AtjTafIyxKO9DX2EEcdOgz3Op5dhwKQFhMJgdfIRbYHBUF+SU97Y6P+zyLXNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.10.1",
|
"version": "24.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||||
|
|
@ -278,6 +311,12 @@
|
||||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
|
@ -335,6 +374,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/big-integer": {
|
||||||
|
"version": "1.6.52",
|
||||||
|
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
|
||||||
|
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
|
|
@ -447,6 +495,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
|
@ -563,6 +617,12 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
|
@ -827,6 +887,12 @@
|
||||||
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.transform": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/magic-bytes.js": {
|
"node_modules/magic-bytes.js": {
|
||||||
"version": "1.12.1",
|
"version": "1.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz",
|
||||||
|
|
@ -1057,6 +1123,19 @@
|
||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/esm/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.18.3",
|
"version": "8.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||||
|
|
@ -1077,6 +1156,15 @@
|
||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.25.76",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,19 @@
|
||||||
"dev": "nodemon bot.js",
|
"dev": "nodemon bot.js",
|
||||||
"register-commands": "node scripts/register-commands.js"
|
"register-commands": "node scripts/register-commands.js"
|
||||||
},
|
},
|
||||||
|
"keywords": [
|
||||||
|
"discord",
|
||||||
|
"bot",
|
||||||
|
"aethex",
|
||||||
|
"role-management",
|
||||||
|
"sentinel",
|
||||||
|
"security",
|
||||||
|
"discord.js"
|
||||||
|
],
|
||||||
|
"author": "AeThex Team",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@discord/embedded-app-sdk": "^2.4.0",
|
||||||
"@supabase/supabase-js": "^2.38.0",
|
"@supabase/supabase-js": "^2.38.0",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"discord.js": "^14.13.0",
|
"discord.js": "^14.13.0",
|
||||||
|
|
|
||||||
96
replit.md
96
replit.md
|
|
@ -1,11 +1,12 @@
|
||||||
# AeThex Unified Bot
|
# AeThex Unified Bot
|
||||||
|
|
||||||
A single Discord bot combining community features and enterprise security (Sentinel).
|
A complete Discord bot combining AeThex community features and Sentinel enterprise security in one instance.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
AeThex Unified Bot handles both community features AND security in one instance:
|
AeThex Unified Bot handles both community features AND security:
|
||||||
|
|
||||||
|
- **Community Features**: User verification, profile linking, realm selection, leaderboards, community posts
|
||||||
- **Sentinel Security**: Anti-nuke protection with RAM-based heat tracking
|
- **Sentinel Security**: Anti-nuke protection with RAM-based heat tracking
|
||||||
- **Federation Sync**: Cross-server role synchronization across 5 realms
|
- **Federation Sync**: Cross-server role synchronization across 5 realms
|
||||||
- **Ticket System**: Support tickets with automatic channel creation
|
- **Ticket System**: Support tickets with automatic channel creation
|
||||||
|
|
@ -15,24 +16,38 @@ AeThex Unified Bot handles both community features AND security in one instance:
|
||||||
|
|
||||||
- **Runtime**: Node.js 20
|
- **Runtime**: Node.js 20
|
||||||
- **Framework**: discord.js v14
|
- **Framework**: discord.js v14
|
||||||
- **Database**: Supabase (optional, for user verification)
|
- **Database**: Supabase (optional - for user verification and community features)
|
||||||
- **Health Endpoint**: HTTP server on port 8080
|
- **Health Endpoint**: HTTP server on port 8080
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
aethex-bot/
|
aethex-bot/
|
||||||
├── bot.js # Main entry point
|
├── bot.js # Main entry point (merged: original + Sentinel)
|
||||||
├── package.json
|
├── package.json
|
||||||
├── .env.example
|
├── .env.example # Complete environment template
|
||||||
|
├── Dockerfile # Docker deployment config
|
||||||
|
├── discloud.config # DisCloud hosting config
|
||||||
|
├── DEPLOYMENT_GUIDE.md # Deployment documentation
|
||||||
├── commands/
|
├── commands/
|
||||||
│ ├── admin.js # /admin status|heat|servers|threats|federation
|
│ ├── admin.js # /admin status|heat|servers|threats|federation
|
||||||
│ ├── federation.js # /federation link|unlink|list
|
│ ├── federation.js # /federation link|unlink|list
|
||||||
|
│ ├── help.js # /help - command list
|
||||||
|
│ ├── leaderboard.js # /leaderboard - top contributors
|
||||||
|
│ ├── post.js # /post - community feed posts
|
||||||
|
│ ├── profile.js # /profile - view linked profile
|
||||||
|
│ ├── refresh-roles.js # /refresh-roles - sync roles
|
||||||
|
│ ├── set-realm.js # /set-realm - choose primary realm
|
||||||
|
│ ├── stats.js # /stats - user statistics
|
||||||
│ ├── status.js # /status - network overview
|
│ ├── status.js # /status - network overview
|
||||||
│ └── ticket.js # /ticket create|close
|
│ ├── ticket.js # /ticket create|close
|
||||||
|
│ ├── unlink.js # /unlink - disconnect account
|
||||||
|
│ ├── verify-role.js # /verify-role - check roles
|
||||||
|
│ └── verify.js # /verify - link account
|
||||||
├── events/
|
├── events/
|
||||||
│ └── guildMemberUpdate.js # Federation role sync listener
|
│ └── messageCreate.js # Message event handler
|
||||||
├── listeners/
|
├── listeners/
|
||||||
|
│ ├── feedSync.js # Community feed sync
|
||||||
│ └── sentinel/
|
│ └── sentinel/
|
||||||
│ ├── antiNuke.js # Channel delete monitor
|
│ ├── antiNuke.js # Channel delete monitor
|
||||||
│ ├── roleDelete.js # Role delete monitor
|
│ ├── roleDelete.js # Role delete monitor
|
||||||
|
|
@ -42,8 +57,23 @@ aethex-bot/
|
||||||
└── register-commands.js # Slash command registration
|
└── register-commands.js # Slash command registration
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands (14 Total)
|
||||||
|
|
||||||
|
### Community Commands (10)
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/verify` | Link your Discord account to AeThex |
|
||||||
|
| `/unlink` | Disconnect your Discord from AeThex |
|
||||||
|
| `/profile` | View your linked AeThex profile |
|
||||||
|
| `/set-realm` | Choose your primary realm |
|
||||||
|
| `/verify-role` | Check your assigned Discord roles |
|
||||||
|
| `/refresh-roles` | Sync roles based on AeThex profile |
|
||||||
|
| `/stats` | View your AeThex statistics |
|
||||||
|
| `/leaderboard` | View top contributors |
|
||||||
|
| `/post` | Create a community feed post |
|
||||||
|
| `/help` | View all bot commands |
|
||||||
|
|
||||||
|
### Sentinel Commands (4)
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `/admin status` | View bot status and statistics |
|
| `/admin status` | View bot status and statistics |
|
||||||
|
|
@ -60,7 +90,7 @@ aethex-bot/
|
||||||
|
|
||||||
## Sentinel Security System
|
## Sentinel Security System
|
||||||
|
|
||||||
The anti-nuke system uses RAM-based heat tracking for instant response:
|
Anti-nuke system using RAM-based heat tracking for instant response:
|
||||||
|
|
||||||
- **Heat Threshold**: 3 dangerous actions in 10 seconds triggers auto-ban
|
- **Heat Threshold**: 3 dangerous actions in 10 seconds triggers auto-ban
|
||||||
- **Monitored Actions**: Channel delete, role delete, member ban, member kick
|
- **Monitored Actions**: Channel delete, role delete, member ban, member kick
|
||||||
|
|
@ -69,32 +99,39 @@ The anti-nuke system uses RAM-based heat tracking for instant response:
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
Required:
|
### Required
|
||||||
- `DISCORD_TOKEN` or `DISCORD_BOT_TOKEN` - Bot token
|
- `DISCORD_BOT_TOKEN` - Bot token from Discord Developer Portal
|
||||||
- `DISCORD_CLIENT_ID` - Application ID (currently: 578971245454950421)
|
- `DISCORD_CLIENT_ID` - Application ID (e.g., 578971245454950421)
|
||||||
|
|
||||||
Optional - Federation:
|
### Optional - Supabase (for community features)
|
||||||
|
- `SUPABASE_URL` - Supabase project URL
|
||||||
|
- `SUPABASE_SERVICE_ROLE` - Supabase service role key
|
||||||
|
|
||||||
|
### Optional - Federation
|
||||||
- `HUB_GUILD_ID` - Main hub server
|
- `HUB_GUILD_ID` - Main hub server
|
||||||
- `LABS_GUILD_ID`, `GAMEFORGE_GUILD_ID`, `CORP_GUILD_ID`, `FOUNDATION_GUILD_ID`
|
- `LABS_GUILD_ID`, `GAMEFORGE_GUILD_ID`, `CORP_GUILD_ID`, `FOUNDATION_GUILD_ID`
|
||||||
|
|
||||||
Optional - Security:
|
### Optional - Security
|
||||||
- `WHITELISTED_USERS` - Comma-separated user IDs to skip heat tracking
|
- `WHITELISTED_USERS` - Comma-separated user IDs to skip heat tracking
|
||||||
- `ALERT_CHANNEL_ID` - Channel for security alerts
|
- `ALERT_CHANNEL_ID` - Channel for security alerts
|
||||||
|
|
||||||
Optional - Supabase:
|
### Optional - Feed Sync
|
||||||
- `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE` - For user verification features
|
- `DISCORD_FEED_CHANNEL_ID` - Channel for community feed
|
||||||
|
- `DISCORD_FEED_GUILD_ID` - Guild for community feed
|
||||||
|
- `DISCORD_MAIN_CHAT_CHANNELS` - Comma-separated channel IDs
|
||||||
|
|
||||||
## Health Endpoint
|
## Health Endpoints
|
||||||
|
|
||||||
**GET /health** (port 8080)
|
**GET /health** (port 8080)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "online",
|
"status": "online",
|
||||||
"guilds": 5,
|
"guilds": 8,
|
||||||
"commands": 4,
|
"commands": 14,
|
||||||
"uptime": 3600,
|
"uptime": 3600,
|
||||||
"heatMapSize": 0,
|
"heatMapSize": 0,
|
||||||
"timestamp": "2025-12-07T22:15:00.000Z"
|
"supabaseConnected": false,
|
||||||
|
"timestamp": "2025-12-07T23:00:00.000Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -114,13 +151,22 @@ Optional - Supabase:
|
||||||
```bash
|
```bash
|
||||||
cd aethex-bot
|
cd aethex-bot
|
||||||
npm install
|
npm install
|
||||||
node scripts/register-commands.js # Register slash commands (run once)
|
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Commands are registered automatically on startup or via POST to `/register-commands`.
|
||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
- Bot is running and connected to 5 servers
|
- Bot running as AeThex#9389
|
||||||
- All 4 commands registered (/admin, /federation, /status, /ticket)
|
- Connected to 8 servers
|
||||||
- Sentinel listeners active (channel/role delete, ban/kick monitoring)
|
- 14 commands loaded
|
||||||
- Health endpoint available at port 8080
|
- 4 Sentinel listeners active
|
||||||
|
- Health endpoint on port 8080
|
||||||
|
- Supabase optional (community features limited when not configured)
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
- **Name**: AeThex Unified Bot
|
||||||
|
- **Command**: `cd aethex-bot && npm start`
|
||||||
|
- **Status**: Running
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue