From b178664f99e7451c9d0c0cd73cfec0f6c83b93c0 Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Sun, 7 Dec 2025 23:41:11 +0000 Subject: [PATCH] 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 --- .replit | 4 + aethex-bot/.env.example | 49 +- aethex-bot/DEPLOYMENT_GUIDE.md | 211 +++++++ aethex-bot/Dockerfile | 22 + aethex-bot/bot.js | 892 ++++++++++++++++++++++++--- aethex-bot/commands/leaderboard.js | 9 +- aethex-bot/commands/post.js | 9 +- aethex-bot/commands/profile.js | 9 +- aethex-bot/commands/refresh-roles.js | 12 +- aethex-bot/commands/set-realm.js | 9 +- aethex-bot/commands/stats.js | 9 +- aethex-bot/commands/unlink.js | 9 +- aethex-bot/commands/verify-role.js | 9 +- aethex-bot/commands/verify.js | 9 +- aethex-bot/discloud.config | 10 + aethex-bot/listeners/feedSync.js | 16 +- aethex-bot/package-lock.json | 88 +++ aethex-bot/package.json | 12 + replit.md | 96 ++- 19 files changed, 1285 insertions(+), 199 deletions(-) create mode 100644 aethex-bot/DEPLOYMENT_GUIDE.md create mode 100644 aethex-bot/Dockerfile create mode 100644 aethex-bot/discloud.config diff --git a/.replit b/.replit index 33231a5..7d8dd2f 100644 --- a/.replit +++ b/.replit @@ -22,6 +22,10 @@ externalPort = 80 localPort = 8080 externalPort = 8080 +[[ports]] +localPort = 38431 +externalPort = 3000 + [workflows] runButton = "Project" diff --git a/aethex-bot/.env.example b/aethex-bot/.env.example index 45b42f1..aaf8657 100644 --- a/aethex-bot/.env.example +++ b/aethex-bot/.env.example @@ -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 diff --git a/aethex-bot/DEPLOYMENT_GUIDE.md b/aethex-bot/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..c90a3aa --- /dev/null +++ b/aethex-bot/DEPLOYMENT_GUIDE.md @@ -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= +DISCORD_CLIENT_ID= +DISCORD_PUBLIC_KEY= +SUPABASE_URL= +SUPABASE_SERVICE_ROLE= +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 # + 📡 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:/// +``` + +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:** `` +**Bot Status:** `` +**Last Updated:** `` diff --git a/aethex-bot/Dockerfile b/aethex-bot/Dockerfile new file mode 100644 index 0000000..279b52f --- /dev/null +++ b/aethex-bot/Dockerfile @@ -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"] diff --git a/aethex-bot/bot.js b/aethex-bot/bot.js index 39ec557..e991271 100644 --- a/aethex-bot/bot.js +++ b/aethex-bot/bot.js @@ -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(` + + + + Register Discord Commands + + + +
+

Discord Commands Registration

+

Click to register all ${COMMANDS_TO_REGISTER.length} slash commands

+ +
Registering... please wait...
+
+
+ + + + `); + 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); }); diff --git a/aethex-bot/commands/leaderboard.js b/aethex-bot/commands/leaderboard.js index 2f6fcfd..a7157d4 100644 --- a/aethex-bot/commands/leaderboard.js +++ b/aethex-bot/commands/leaderboard.js @@ -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"; diff --git a/aethex-bot/commands/post.js b/aethex-bot/commands/post.js index 04c91dd..08a1e7f 100644 --- a/aethex-bot/commands/post.js +++ b/aethex-bot/commands/post.js @@ -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 diff --git a/aethex-bot/commands/profile.js b/aethex-bot/commands/profile.js index 4faab7d..d54f926 100644 --- a/aethex-bot/commands/profile.js +++ b/aethex-bot/commands/profile.js @@ -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 diff --git a/aethex-bot/commands/refresh-roles.js b/aethex-bot/commands/refresh-roles.js index a159d11..9f138da 100644 --- a/aethex-bot/commands/refresh-roles.js +++ b/aethex-bot/commands/refresh-roles.js @@ -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") diff --git a/aethex-bot/commands/set-realm.js b/aethex-bot/commands/set-realm.js index f8e8204..2205d24 100644 --- a/aethex-bot/commands/set-realm.js +++ b/aethex-bot/commands/set-realm.js @@ -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 diff --git a/aethex-bot/commands/stats.js b/aethex-bot/commands/stats.js index 6931f9d..eef0a18 100644 --- a/aethex-bot/commands/stats.js +++ b/aethex-bot/commands/stats.js @@ -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 diff --git a/aethex-bot/commands/unlink.js b/aethex-bot/commands/unlink.js index 4812ea6..f179310 100644 --- a/aethex-bot/commands/unlink.js +++ b/aethex-bot/commands/unlink.js @@ -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 diff --git a/aethex-bot/commands/verify-role.js b/aethex-bot/commands/verify-role.js index 1677d58..f71d587 100644 --- a/aethex-bot/commands/verify-role.js +++ b/aethex-bot/commands/verify-role.js @@ -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 diff --git a/aethex-bot/commands/verify.js b/aethex-bot/commands/verify.js index a4bbf1b..225869a 100644 --- a/aethex-bot/commands/verify.js +++ b/aethex-bot/commands/verify.js @@ -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 diff --git a/aethex-bot/discloud.config b/aethex-bot/discloud.config new file mode 100644 index 0000000..fe114e6 --- /dev/null +++ b/aethex-bot/discloud.config @@ -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 diff --git a/aethex-bot/listeners/feedSync.js b/aethex-bot/listeners/feedSync.js index b8168c4..ed486ab 100644 --- a/aethex-bot/listeners/feedSync.js +++ b/aethex-bot/listeners/feedSync.js @@ -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; diff --git a/aethex-bot/package-lock.json b/aethex-bot/package-lock.json index 2fbca63..da33aa2 100644 --- a/aethex-bot/package-lock.json +++ b/aethex-bot/package-lock.json @@ -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" + } } } } diff --git a/aethex-bot/package.json b/aethex-bot/package.json index a70d25b..fb82ccc 100644 --- a/aethex-bot/package.json +++ b/aethex-bot/package.json @@ -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", diff --git a/replit.md b/replit.md index 57b6f72..79a423b 100644 --- a/replit.md +++ b/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