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