diff --git a/attached_assets/bot1/discord-bot/.env.example b/attached_assets/bot1/discord-bot/.env.example new file mode 100644 index 0000000..a4ddc91 --- /dev/null +++ b/attached_assets/bot1/discord-bot/.env.example @@ -0,0 +1,23 @@ +# Discord Bot Configuration +DISCORD_BOT_TOKEN=your_bot_token_here +DISCORD_CLIENT_ID=your_client_id_here +DISCORD_PUBLIC_KEY=your_public_key_here + +# Supabase Configuration +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_SERVICE_ROLE=your_service_role_key_here + +# 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 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 diff --git a/attached_assets/bot1/discord-bot/DEPLOYMENT_GUIDE.md b/attached_assets/bot1/discord-bot/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..c90a3aa --- /dev/null +++ b/attached_assets/bot1/discord-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/attached_assets/bot1/discord-bot/Dockerfile b/attached_assets/bot1/discord-bot/Dockerfile new file mode 100644 index 0000000..279b52f --- /dev/null +++ b/attached_assets/bot1/discord-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/attached_assets/bot1/discord-bot/bot.js b/attached_assets/bot1/discord-bot/bot.js new file mode 100644 index 0000000..e015b6d --- /dev/null +++ b/attached_assets/bot1/discord-bot/bot.js @@ -0,0 +1,522 @@ +const { + Client, + GatewayIntentBits, + REST, + Routes, + Collection, + EmbedBuilder, +} = require("discord.js"); +const { createClient } = require("@supabase/supabase-js"); +const http = require("http"); +const fs = require("fs"); +const path = require("path"); +require("dotenv").config(); + +// Validate environment variables +const requiredEnvVars = [ + "DISCORD_BOT_TOKEN", + "DISCORD_CLIENT_ID", + "SUPABASE_URL", + "SUPABASE_SERVICE_ROLE", +]; + +const missingVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); +if (missingVars.length > 0) { + console.error( + "โŒ FATAL ERROR: Missing required environment variables:", + missingVars.join(", "), + ); + console.error("\nPlease set these in your Discloud/hosting environment:"); + missingVars.forEach((envVar) => { + console.error(` - ${envVar}`); + }); + process.exit(1); +} + +// Validate token format +const token = process.env.DISCORD_BOT_TOKEN; +if (!token || token.length < 20) { + console.error("โŒ FATAL ERROR: DISCORD_BOT_TOKEN is empty or invalid"); + console.error(` Length: ${token ? token.length : 0}`); + process.exit(1); +} + +console.log("[Token] Bot token loaded (length: " + token.length + " chars)"); + +// Initialize Discord client with message intents for feed sync +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.DirectMessages, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], +}); + +// Initialize Supabase +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE, +); + +// Store slash commands +client.commands = new Collection(); + +// Load commands from commands directory +const commandsPath = path.join(__dirname, "commands"); +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}`); + } +} + +// Load event handlers from events directory +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 listener: ${event.name}`); + } + } +} + +// Bot ready event +client.once("ready", () => { + console.log(`โœ… Bot logged in as ${client.user.tag}`); + console.log(`๐Ÿ“ก Listening in ${client.guilds.cache.size} server(s)`); + + // Set bot status + client.user.setActivity("/verify to link your AeThex account", { + type: "LISTENING", + }); +}); + +// Slash command interaction handler +client.on("interactionCreate", async (interaction) => { + if (!interaction.isChatInputCommand()) return; + + const command = client.commands.get(interaction.commandName); + + if (!command) { + console.warn( + `โš ๏ธ No command matching ${interaction.commandName} was found.`, + ); + return; + } + + try { + await command.execute(interaction, supabase, client); + } catch (error) { + console.error(`โŒ Error executing ${interaction.commandName}:`, error); + + const errorEmbed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Command Error") + .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 }); + } else { + await interaction.reply({ embeds: [errorEmbed], ephemeral: true }); + } + } +}); + +// IMPORTANT: Commands are now registered via a separate script +// Run this ONCE during deployment: npm run register-commands +// This prevents Error 50240 (Entry Point conflict) when Activities are enabled +// The bot will simply load and listen for the already-registered commands + +// Define all commands for registration +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", + }, +]; + +// Function to register commands with Discord +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 { + // Try bulk update first + 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) { + // Handle Error 50240 (Entry Point conflict) + 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 }; + } +} + +// Start HTTP health check server +const healthPort = process.env.HEALTH_PORT || 8044; +http + .createServer((req, res) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + 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()), + timestamp: new Date().toISOString(), + }), + ); + return; + } + + if (req.url === "/register-commands") { + if (req.method === "GET") { + // Show HTML form with button + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(` + + + + Register Discord Commands + + + +
+

๐Ÿค– Discord Commands Registration

+

Click the button below to register all Discord slash commands (/verify, /set-realm, /profile, /unlink, /verify-role)

+ + + +
โณ Registering... please wait...
+
+
+ + + + + `); + return; + } + + if (req.method === "POST") { + // Verify admin token if provided + const authHeader = req.headers.authorization; + const adminToken = process.env.DISCORD_ADMIN_REGISTER_TOKEN; + + if (adminToken && authHeader !== `Bearer ${adminToken}`) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized" })); + return; + } + + // Register commands + 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`, + ); + }); + +// Login with error handling +client.login(process.env.DISCORD_BOT_TOKEN).catch((error) => { + console.error("โŒ FATAL ERROR: Failed to login to Discord"); + console.error(` Error Code: ${error.code}`); + console.error(` Error Message: ${error.message}`); + + if (error.code === "TokenInvalid") { + console.error("\nโš ๏ธ DISCORD_BOT_TOKEN is invalid!"); + console.error(" Possible causes:"); + console.error(" 1. Token has been revoked by Discord"); + console.error(" 2. Token has expired"); + console.error(" 3. Token format is incorrect"); + console.error( + "\n Solution: Get a new bot token from Discord Developer Portal", + ); + console.error(" https://discord.com/developers/applications"); + } + + process.exit(1); +}); + +client.once("ready", () => { + console.log(`โœ… Bot logged in as ${client.user.tag}`); + console.log(`๐Ÿ“ก Listening in ${client.guilds.cache.size} server(s)`); + console.log("โ„น๏ธ Commands are registered via: npm run register-commands"); + + // Set bot status + client.user.setActivity("/verify to link your AeThex account", { + type: "LISTENING", + }); +}); + +// Error handling +process.on("unhandledRejection", (error) => { + console.error("โŒ Unhandled Promise Rejection:", error); +}); + +process.on("uncaughtException", (error) => { + console.error("โŒ Uncaught Exception:", error); + process.exit(1); +}); + +module.exports = client; diff --git a/attached_assets/bot1/discord-bot/commands/profile.js b/attached_assets/bot1/discord-bot/commands/profile.js new file mode 100644 index 0000000..035f251 --- /dev/null +++ b/attached_assets/bot1/discord-bot/commands/profile.js @@ -0,0 +1,93 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("profile") + .setDescription("View your AeThex profile in Discord"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("*") + .eq("id", link.user_id) + .single(); + + if (!profile) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Profile Not Found") + .setDescription("Your AeThex profile could not be found."); + + return await interaction.editReply({ embeds: [embed] }); + } + + const armEmojis = { + labs: "๐Ÿงช", + gameforge: "๐ŸŽฎ", + corp: "๐Ÿ’ผ", + foundation: "๐Ÿค", + devlink: "๐Ÿ’ป", + }; + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle(`${profile.full_name || "AeThex User"}'s Profile`) + .setThumbnail( + profile.avatar_url || "https://aethex.dev/placeholder.svg", + ) + .addFields( + { + name: "๐Ÿ‘ค Username", + value: profile.username || "N/A", + inline: true, + }, + { + name: `${armEmojis[link.primary_arm] || "โš”๏ธ"} Primary Realm`, + value: link.primary_arm || "Not set", + inline: true, + }, + { + name: "๐Ÿ“Š Role", + value: profile.user_type || "community_member", + inline: true, + }, + { name: "๐Ÿ“ Bio", value: profile.bio || "No bio set", inline: false }, + ) + .addFields({ + name: "๐Ÿ”— Links", + value: `[Visit Full Profile](https://aethex.dev/creators/${profile.username})`, + }) + .setFooter({ text: "AeThex | Your Web3 Creator Hub" }); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Profile command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to fetch profile. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot1/discord-bot/commands/refresh-roles.js b/attached_assets/bot1/discord-bot/commands/refresh-roles.js new file mode 100644 index 0000000..459bd79 --- /dev/null +++ b/attached_assets/bot1/discord-bot/commands/refresh-roles.js @@ -0,0 +1,72 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const { assignRoleByArm, getUserArm } = require("../utils/roleManager"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("refresh-roles") + .setDescription( + "Refresh your Discord roles based on your current AeThex settings", + ), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + // Check if user is linked + const { data: link } = await supabase + .from("discord_links") + .select("primary_arm") + .eq("discord_id", interaction.user.id) + .maybeSingle(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + if (!link.primary_arm) { + const embed = new EmbedBuilder() + .setColor(0xffaa00) + .setTitle("โš ๏ธ No Realm Set") + .setDescription( + "You haven't set your primary realm yet.\nUse `/set-realm` to choose one.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + // Assign role based on current primary arm + const roleAssigned = await assignRoleByArm( + interaction.guild, + interaction.user.id, + link.primary_arm, + supabase, + ); + + const embed = new EmbedBuilder() + .setColor(roleAssigned ? 0x00ff00 : 0xffaa00) + .setTitle("โœ… Roles Refreshed") + .setDescription( + roleAssigned + ? `Your Discord roles have been synced with your AeThex account.\n\nPrimary Realm: **${link.primary_arm}**` + : `Your roles could not be automatically assigned.\n\nPrimary Realm: **${link.primary_arm}**\n\nโš ๏ธ Please contact an admin to set up the role mapping for this server.`, + ); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Refresh-roles command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to refresh roles. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot1/discord-bot/commands/set-realm.js b/attached_assets/bot1/discord-bot/commands/set-realm.js new file mode 100644 index 0000000..c1af120 --- /dev/null +++ b/attached_assets/bot1/discord-bot/commands/set-realm.js @@ -0,0 +1,139 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + StringSelectMenuBuilder, + ActionRowBuilder, +} = require("discord.js"); +const { assignRoleByArm } = require("../utils/roleManager"); + +const REALMS = [ + { value: "labs", label: "๐Ÿงช Labs", description: "Research & Development" }, + { + value: "gameforge", + label: "๐ŸŽฎ GameForge", + description: "Game Development", + }, + { value: "corp", label: "๐Ÿ’ผ Corp", description: "Enterprise Solutions" }, + { + value: "foundation", + label: "๐Ÿค Foundation", + description: "Community & Education", + }, + { + value: "devlink", + label: "๐Ÿ’ป Dev-Link", + description: "Professional Networking", + }, +]; + +module.exports = { + data: new SlashCommandBuilder() + .setName("set-realm") + .setDescription("Set your primary AeThex realm/arm"), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const select = new StringSelectMenuBuilder() + .setCustomId("select_realm") + .setPlaceholder("Choose your primary realm") + .addOptions( + REALMS.map((realm) => ({ + label: realm.label, + description: realm.description, + value: realm.value, + default: realm.value === link.primary_arm, + })), + ); + + const row = new ActionRowBuilder().addComponents(select); + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("โš”๏ธ Choose Your Realm") + .setDescription( + "Select your primary AeThex realm. This determines your main Discord role.", + ) + .addFields({ + name: "Current Realm", + value: link.primary_arm || "Not set", + }); + + await interaction.editReply({ embeds: [embed], components: [row] }); + + const filter = (i) => + i.user.id === interaction.user.id && i.customId === "select_realm"; + const collector = interaction.channel.createMessageComponentCollector({ + filter, + time: 60000, + }); + + collector.on("collect", async (i) => { + const selectedRealm = i.values[0]; + + await supabase + .from("discord_links") + .update({ primary_arm: selectedRealm }) + .eq("discord_id", interaction.user.id); + + const realm = REALMS.find((r) => r.value === selectedRealm); + + // Assign Discord role based on selected realm + const roleAssigned = await assignRoleByArm( + interaction.guild, + interaction.user.id, + selectedRealm, + supabase, + ); + + const roleStatus = roleAssigned + ? "โœ… Discord role assigned!" + : "โš ๏ธ No role mapping found for this realm in this server."; + + const confirmEmbed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Realm Set") + .setDescription( + `Your primary realm is now **${realm.label}**\n\n${roleStatus}`, + ); + + await i.update({ embeds: [confirmEmbed], components: [] }); + }); + + collector.on("end", (collected) => { + if (collected.size === 0) { + interaction.editReply({ + content: "Realm selection timed out.", + components: [], + }); + } + }); + } catch (error) { + console.error("Set-realm command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to update realm. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot1/discord-bot/commands/unlink.js b/attached_assets/bot1/discord-bot/commands/unlink.js new file mode 100644 index 0000000..ac06d2a --- /dev/null +++ b/attached_assets/bot1/discord-bot/commands/unlink.js @@ -0,0 +1,75 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("unlink") + .setDescription("Unlink your Discord account from AeThex"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("*") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โ„น๏ธ Not Linked") + .setDescription("Your Discord account is not linked to AeThex."); + + return await interaction.editReply({ embeds: [embed] }); + } + + // Delete the link + await supabase + .from("discord_links") + .delete() + .eq("discord_id", interaction.user.id); + + // Remove Discord roles from user + const guild = interaction.guild; + const member = await guild.members.fetch(interaction.user.id); + + // Find and remove all AeThex-related roles + const rolesToRemove = member.roles.cache.filter( + (role) => + role.name.includes("Labs") || + role.name.includes("GameForge") || + role.name.includes("Corp") || + role.name.includes("Foundation") || + role.name.includes("Dev-Link") || + role.name.includes("Premium") || + role.name.includes("Creator"), + ); + + for (const [, role] of rolesToRemove) { + try { + await member.roles.remove(role); + } catch (e) { + console.warn(`Could not remove role ${role.name}`); + } + } + + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Account Unlinked") + .setDescription( + "Your Discord account has been unlinked from AeThex.\nAll associated roles have been removed.", + ); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Unlink command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to unlink account. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot1/discord-bot/commands/verify-role.js b/attached_assets/bot1/discord-bot/commands/verify-role.js new file mode 100644 index 0000000..1b7e6b9 --- /dev/null +++ b/attached_assets/bot1/discord-bot/commands/verify-role.js @@ -0,0 +1,97 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("verify-role") + .setDescription("Check your AeThex-assigned Discord roles"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("user_type") + .eq("id", link.user_id) + .single(); + + const { data: mappings } = await supabase + .from("discord_role_mappings") + .select("discord_role") + .eq("arm", link.primary_arm) + .eq("user_type", profile?.user_type || "community_member"); + + const member = await interaction.guild.members.fetch(interaction.user.id); + const aethexRoles = member.roles.cache.filter( + (role) => + role.name.includes("Labs") || + role.name.includes("GameForge") || + role.name.includes("Corp") || + role.name.includes("Foundation") || + role.name.includes("Dev-Link") || + role.name.includes("Premium") || + role.name.includes("Creator"), + ); + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("๐Ÿ” Your AeThex Roles") + .addFields( + { + name: "โš”๏ธ Primary Realm", + value: link.primary_arm || "Not set", + inline: true, + }, + { + name: "๐Ÿ‘ค User Type", + value: profile?.user_type || "community_member", + inline: true, + }, + { + name: "๐ŸŽญ Discord Roles", + value: + aethexRoles.size > 0 + ? aethexRoles.map((r) => r.name).join(", ") + : "None assigned yet", + }, + { + name: "๐Ÿ“‹ Expected Roles", + value: + mappings?.length > 0 + ? mappings.map((m) => m.discord_role).join(", ") + : "No mappings found", + }, + ) + .setFooter({ + text: "Roles are assigned automatically based on your AeThex profile", + }); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Verify-role command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to verify roles. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot1/discord-bot/commands/verify.js b/attached_assets/bot1/discord-bot/commands/verify.js new file mode 100644 index 0000000..4e5e6c4 --- /dev/null +++ b/attached_assets/bot1/discord-bot/commands/verify.js @@ -0,0 +1,84 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, +} = require("discord.js"); +const { syncRolesAcrossGuilds } = require("../utils/roleManager"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("verify") + .setDescription("Link your Discord account to your AeThex account"), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: existingLink } = await supabase + .from("discord_links") + .select("*") + .eq("discord_id", interaction.user.id) + .single(); + + if (existingLink) { + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Already Linked") + .setDescription( + `Your Discord account is already linked to AeThex (User ID: ${existingLink.user_id})`, + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + // Generate verification code + const verificationCode = Math.random() + .toString(36) + .substring(2, 8) + .toUpperCase(); + const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes + + // Store verification code + await supabase.from("discord_verifications").insert({ + discord_id: interaction.user.id, + verification_code: verificationCode, + expires_at: expiresAt.toISOString(), + }); + + const verifyUrl = `https://aethex.dev/discord-verify?code=${verificationCode}`; + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("๐Ÿ”— Link Your AeThex Account") + .setDescription( + "Click the button below to link your Discord account to AeThex.", + ) + .addFields( + { name: "โฑ๏ธ Expires In", value: "15 minutes" }, + { name: "๐Ÿ“ Verification Code", value: `\`${verificationCode}\`` }, + ) + .setFooter({ text: "Your security code will expire in 15 minutes" }); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("Link Account") + .setStyle(ButtonStyle.Link) + .setURL(verifyUrl), + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); + } catch (error) { + console.error("Verify command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription( + "Failed to generate verification code. Please try again.", + ); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot1/discord-bot/discloud.config b/attached_assets/bot1/discord-bot/discloud.config new file mode 100644 index 0000000..fe114e6 --- /dev/null +++ b/attached_assets/bot1/discord-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/attached_assets/bot1/discord-bot/events/messageCreate-announcements.js b/attached_assets/bot1/discord-bot/events/messageCreate-announcements.js new file mode 100644 index 0000000..0193468 --- /dev/null +++ b/attached_assets/bot1/discord-bot/events/messageCreate-announcements.js @@ -0,0 +1,237 @@ +const { createClient } = require("@supabase/supabase-js"); + +// Initialize Supabase +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE, +); + +const API_BASE = process.env.VITE_API_BASE || "https://api.aethex.dev"; + +// Channel IDs for syncing +const ANNOUNCEMENT_CHANNELS = process.env.DISCORD_ANNOUNCEMENT_CHANNELS + ? process.env.DISCORD_ANNOUNCEMENT_CHANNELS.split(",") + : ["1435667453244866702"]; // Default to feed channel if env not set + +// Arm affiliation mapping based on guild/channel name +const getArmAffiliation = (message) => { + const guildName = message.guild?.name?.toLowerCase() || ""; + const channelName = message.channel?.name?.toLowerCase() || ""; + + const searchString = `${guildName} ${channelName}`.toLowerCase(); + + if (searchString.includes("gameforge")) return "gameforge"; + if (searchString.includes("corp")) return "corp"; + if (searchString.includes("foundation")) return "foundation"; + if (searchString.includes("devlink") || searchString.includes("dev-link")) + return "devlink"; + if (searchString.includes("nexus")) return "nexus"; + if (searchString.includes("staff")) return "staff"; + + return "labs"; // Default +}; + +module.exports = { + name: "messageCreate", + async execute(message, client, supabase) { + try { + // Ignore bot messages + if (message.author.bot) return; + + // Only process messages in announcement channels + if (!ANNOUNCEMENT_CHANNELS.includes(message.channelId)) { + return; + } + + // Skip empty messages + if (!message.content && message.attachments.size === 0) { + return; + } + + console.log( + `[Announcements Sync] Processing message from ${message.author.tag} in #${message.channel.name}`, + ); + + // Get or create system admin user for announcements + let adminUser = await supabase + .from("user_profiles") + .select("id") + .eq("username", "aethex-announcements") + .single(); + + let authorId = adminUser.data?.id; + + if (!authorId) { + // Create a system user if it doesn't exist + const { data: newUser } = await supabase + .from("user_profiles") + .insert({ + username: "aethex-announcements", + full_name: "AeThex Announcements", + avatar_url: "https://aethex.dev/logo.png", + }) + .select("id"); + + authorId = newUser?.[0]?.id; + } + + if (!authorId) { + console.error("[Announcements Sync] Could not get author ID"); + return; + } + + // Prepare message content + let content = message.content || "Announcement from Discord"; + + // Handle embeds (convert to text) + if (message.embeds.length > 0) { + const embed = message.embeds[0]; + if (embed.title) content = embed.title + "\n\n" + content; + if (embed.description) content += "\n\n" + embed.description; + } + + // Handle attachments (images, videos) + let mediaUrl = null; + let mediaType = "none"; + + if (message.attachments.size > 0) { + const attachment = message.attachments.first(); + if (attachment) { + mediaUrl = attachment.url; + + const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp"]; + const videoExtensions = [".mp4", ".webm", ".mov", ".avi"]; + + const attachmentLower = attachment.name.toLowerCase(); + + if (imageExtensions.some((ext) => attachmentLower.endsWith(ext))) { + mediaType = "image"; + } else if ( + videoExtensions.some((ext) => attachmentLower.endsWith(ext)) + ) { + mediaType = "video"; + } + } + } + + // Determine arm affiliation + const armAffiliation = getArmAffiliation(message); + + // Prepare post content JSON + const postContent = JSON.stringify({ + text: content, + mediaUrl: mediaUrl, + mediaType: mediaType, + source: "discord", + discord_message_id: message.id, + discord_author: message.author.tag, + }); + + // Create post in AeThex + const { data: createdPost, error: insertError } = await supabase + .from("community_posts") + .insert({ + title: content.substring(0, 100) || "Discord Announcement", + content: postContent, + arm_affiliation: armAffiliation, + author_id: authorId, + tags: ["discord", "announcement"], + category: "announcement", + is_published: true, + likes_count: 0, + comments_count: 0, + }) + .select( + `id, title, content, arm_affiliation, author_id, created_at, likes_count, comments_count, + user_profiles!community_posts_author_id_fkey (id, username, full_name, avatar_url)`, + ); + + if (insertError) { + console.error( + "[Announcements Sync] Failed to create post:", + insertError, + ); + try { + await message.react("โŒ"); + } catch (reactionError) { + console.warn( + "[Announcements Sync] Could not add reaction:", + reactionError, + ); + } + return; + } + + // Sync to Discord feed webhook if configured + if (process.env.DISCORD_FEED_WEBHOOK_URL && createdPost?.[0]) { + try { + const post = createdPost[0]; + const armColors = { + labs: 0xfbbf24, + gameforge: 0x22c55e, + corp: 0x3b82f6, + foundation: 0xef4444, + devlink: 0x06b6d4, + nexus: 0xa855f7, + staff: 0x6366f1, + }; + + const embed = { + title: post.title, + description: content.substring(0, 1024), + color: armColors[armAffiliation] || 0x8b5cf6, + author: { + name: `${message.author.username} (${armAffiliation.toUpperCase()})`, + icon_url: message.author.displayAvatarURL(), + }, + footer: { + text: "Synced from Discord", + }, + timestamp: new Date().toISOString(), + }; + + await fetch(process.env.DISCORD_FEED_WEBHOOK_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "AeThex Community Feed", + embeds: [embed], + }), + }); + } catch (webhookError) { + console.warn( + "[Announcements Sync] Failed to sync to webhook:", + webhookError, + ); + } + } + + console.log( + `[Announcements Sync] โœ… Posted announcement from Discord to AeThex (${armAffiliation})`, + ); + + // React with success emoji + try { + await message.react("โœ…"); + } catch (reactionError) { + console.warn( + "[Announcements Sync] Could not add success reaction:", + reactionError, + ); + } + } catch (error) { + console.error("[Announcements Sync] Unexpected error:", error); + + try { + await message.react("โš ๏ธ"); + } catch (reactionError) { + console.warn( + "[Announcements Sync] Could not add warning reaction:", + reactionError, + ); + } + } + }, +}; diff --git a/attached_assets/bot1/discord-bot/events/messageCreate.js b/attached_assets/bot1/discord-bot/events/messageCreate.js new file mode 100644 index 0000000..344c3b2 --- /dev/null +++ b/attached_assets/bot1/discord-bot/events/messageCreate.js @@ -0,0 +1,320 @@ +const { createClient } = require("@supabase/supabase-js"); + +// Initialize Supabase +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE, +); + +const FEED_CHANNEL_ID = process.env.DISCORD_FEED_CHANNEL_ID; +const FEED_GUILD_ID = process.env.DISCORD_FEED_GUILD_ID; +const API_BASE = process.env.VITE_API_BASE || "https://api.aethex.dev"; + +// Announcement channels to sync to feed +const ANNOUNCEMENT_CHANNELS = process.env.DISCORD_ANNOUNCEMENT_CHANNELS + ? process.env.DISCORD_ANNOUNCEMENT_CHANNELS.split(",").map((id) => id.trim()) + : []; + +// Helper: Get arm affiliation from message context +function getArmAffiliation(message) { + const guildName = message.guild?.name?.toLowerCase() || ""; + const channelName = message.channel?.name?.toLowerCase() || ""; + const searchString = `${guildName} ${channelName}`; + + if (searchString.includes("gameforge")) return "gameforge"; + if (searchString.includes("corp")) return "corp"; + if (searchString.includes("foundation")) return "foundation"; + if (searchString.includes("devlink") || searchString.includes("dev-link")) + return "devlink"; + if (searchString.includes("nexus")) return "nexus"; + if (searchString.includes("staff")) return "staff"; + + return "labs"; +} + +// Handle announcements from designated channels +async function handleAnnouncementSync(message) { + try { + console.log( + `[Announcements] Processing from ${message.author.tag} in #${message.channel.name}`, + ); + + // Get or create system announcement user + let { data: adminUser } = await supabase + .from("user_profiles") + .select("id") + .eq("username", "aethex-announcements") + .single(); + + let authorId = adminUser?.id; + + if (!authorId) { + const { data: newUser } = await supabase + .from("user_profiles") + .insert({ + username: "aethex-announcements", + full_name: "AeThex Announcements", + avatar_url: "https://aethex.dev/logo.png", + }) + .select("id"); + + authorId = newUser?.[0]?.id; + } + + if (!authorId) { + console.error("[Announcements] Could not get author ID"); + await message.react("โŒ"); + return; + } + + // Prepare content + let content = message.content || "Announcement from Discord"; + + // Handle embeds + if (message.embeds.length > 0) { + const embed = message.embeds[0]; + if (embed.title) content = `**${embed.title}**\n\n${content}`; + if (embed.description) content += `\n\n${embed.description}`; + } + + // Handle attachments + let mediaUrl = null; + let mediaType = "none"; + + if (message.attachments.size > 0) { + const attachment = message.attachments.first(); + if (attachment) { + mediaUrl = attachment.url; + const attachmentLower = attachment.name.toLowerCase(); + + if ( + [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) => + attachmentLower.endsWith(ext), + ) + ) { + mediaType = "image"; + } else if ( + [".mp4", ".webm", ".mov", ".avi"].some((ext) => + attachmentLower.endsWith(ext), + ) + ) { + mediaType = "video"; + } + } + } + + // Determine arm + const armAffiliation = getArmAffiliation(message); + + // Prepare post content + const postContent = JSON.stringify({ + text: content, + mediaUrl: mediaUrl, + mediaType: mediaType, + source: "discord", + discord_message_id: message.id, + discord_channel: message.channel.name, + }); + + // Create post + const { data: createdPost, error: insertError } = await supabase + .from("community_posts") + .insert({ + title: content.substring(0, 100) || "Discord Announcement", + content: postContent, + arm_affiliation: armAffiliation, + author_id: authorId, + tags: ["discord", "announcement"], + category: "announcement", + is_published: true, + likes_count: 0, + comments_count: 0, + }) + .select( + `id, title, content, arm_affiliation, author_id, created_at, likes_count, comments_count, + user_profiles!community_posts_author_id_fkey (id, username, full_name, avatar_url)`, + ); + + if (insertError) { + console.error("[Announcements] Post creation failed:", insertError); + await message.react("โŒ"); + return; + } + + console.log(`[Announcements] โœ… Synced to AeThex (${armAffiliation} arm)`); + + await message.react("โœ…"); + } catch (error) { + console.error("[Announcements] Error:", error); + try { + await message.react("โš ๏ธ"); + } catch (e) { + console.warn("[Announcements] Could not react:", e); + } + } +} + +module.exports = { + name: "messageCreate", + async execute(message, client) { + // Ignore bot messages and empty messages + if (message.author.bot) return; + if (!message.content && message.attachments.size === 0) return; + + // Check if this is an announcement to sync + if ( + ANNOUNCEMENT_CHANNELS.length > 0 && + ANNOUNCEMENT_CHANNELS.includes(message.channelId) + ) { + return handleAnnouncementSync(message); + } + + // Check if this is in the feed channel (for user-generated posts) + if (FEED_CHANNEL_ID && message.channelId !== FEED_CHANNEL_ID) { + return; + } + + if (FEED_GUILD_ID && message.guildId !== FEED_GUILD_ID) { + return; + } + + try { + // Get user's linked AeThex account + const { data: linkedAccount, error } = await supabase + .from("discord_links") + .select("user_id") + .eq("discord_user_id", message.author.id) + .single(); + + if (error || !linkedAccount) { + try { + await message.author.send( + "To have your message posted to AeThex, please link your Discord account! Use `/verify` command.", + ); + } catch (dmError) { + console.warn("[Feed Sync] Could not send DM to user:", dmError); + } + return; + } + + // Get user profile + const { data: userProfile, error: profileError } = await supabase + .from("user_profiles") + .select("id, username, full_name, avatar_url") + .eq("id", linkedAccount.user_id) + .single(); + + if (profileError || !userProfile) { + console.error( + "[Feed Sync] Could not fetch user profile:", + profileError, + ); + return; + } + + // Prepare content + let content = message.content || "Shared a message on Discord"; + let mediaUrl = null; + let mediaType = "none"; + + if (message.attachments.size > 0) { + const attachment = message.attachments.first(); + if (attachment) { + mediaUrl = attachment.url; + const attachmentLower = attachment.name.toLowerCase(); + + if ( + [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) => + attachmentLower.endsWith(ext), + ) + ) { + mediaType = "image"; + } else if ( + [".mp4", ".webm", ".mov", ".avi"].some((ext) => + attachmentLower.endsWith(ext), + ) + ) { + mediaType = "video"; + } + } + } + + // Determine arm affiliation + let armAffiliation = "labs"; + const guild = message.guild; + if (guild) { + const guildNameLower = guild.name.toLowerCase(); + if (guildNameLower.includes("gameforge")) armAffiliation = "gameforge"; + else if (guildNameLower.includes("corp")) armAffiliation = "corp"; + else if (guildNameLower.includes("foundation")) + armAffiliation = "foundation"; + else if (guildNameLower.includes("devlink")) armAffiliation = "devlink"; + else if (guildNameLower.includes("nexus")) armAffiliation = "nexus"; + else if (guildNameLower.includes("staff")) armAffiliation = "staff"; + } + + // Prepare post content + const postContent = JSON.stringify({ + text: content, + mediaUrl: mediaUrl, + mediaType: mediaType, + }); + + // Create post + const { data: createdPost, error: insertError } = await supabase + .from("community_posts") + .insert({ + title: content.substring(0, 100) || "Discord Shared Message", + content: postContent, + arm_affiliation: armAffiliation, + author_id: userProfile.id, + tags: ["discord"], + category: null, + is_published: true, + likes_count: 0, + comments_count: 0, + }) + .select( + `id, title, content, arm_affiliation, author_id, created_at, updated_at, likes_count, comments_count, + user_profiles!community_posts_author_id_fkey (id, username, full_name, avatar_url)`, + ); + + if (insertError) { + console.error("[Feed Sync] Failed to create post:", insertError); + try { + await message.react("โŒ"); + } catch (e) { + console.warn("[Feed Sync] Could not add reaction:", e); + } + return; + } + + console.log(`[Feed Sync] โœ… Posted from ${message.author.tag} to AeThex`); + + try { + await message.react("โœ…"); + } catch (reactionError) { + console.warn( + "[Feed Sync] Could not add success reaction:", + reactionError, + ); + } + + try { + await message.author.send( + `โœ… Your message was posted to AeThex! Check it out at https://aethex.dev/feed`, + ); + } catch (dmError) { + console.warn("[Feed Sync] Could not send confirmation DM:", dmError); + } + } catch (error) { + console.error("[Feed Sync] Unexpected error:", error); + + try { + await message.react("โš ๏ธ"); + } catch (e) { + console.warn("[Feed Sync] Could not add warning reaction:", e); + } + } + }, +}; diff --git a/attached_assets/bot1/discord-bot/package-lock.json b/attached_assets/bot1/discord-bot/package-lock.json new file mode 100644 index 0000000..897f4a0 --- /dev/null +++ b/attached_assets/bot1/discord-bot/package-lock.json @@ -0,0 +1,1157 @@ +{ + "name": "aethex-discord-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aethex-discord-bot", + "version": "1.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", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=18.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.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.0.tgz", + "integrity": "sha512-COK0uU6ZaJI+LA67H/rp8IbEkYwlZf3mAoBI5wtPh5G5cbEQGNhVpzINg2f/6+q/YipnNIKy6fJDg6kMUKUw4Q==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.1", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", + "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.1" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.80.0.tgz", + "integrity": "sha512-q2LyCVJGN4p7d92cOI7scWOoNwxJhZuFRwiimSUGJGI5zX7ubf1WUPznwOmYEn8WVo3Io+MyMinA7era6j5KPw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.80.0.tgz", + "integrity": "sha512-0S/k8LRtoblrbzy4ir9m4WuvU/XTkb1EwL/33/oJexCUHCXtsqaPJ3eKfr1GWtNqTa1zryv6sXs3Fpv7lKCsMQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.80.0.tgz", + "integrity": "sha512-yKzehXlRbDoXIQefdRQnvaI9BEogoWIp/7+y/m5enZDKW2IP9aAgq5tU72sThcwftDJvknnIpEHAABG3qviEng==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.80.0.tgz", + "integrity": "sha512-cXK6Gs4UDylN8oz40omi01QK0cSCBVj0efXC1WodpENTuDnrkUs28W8/eslEnAtlawaVtikC1Q92mpz9+o85Mg==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.80.0.tgz", + "integrity": "sha512-Iepod83h2WoMCaLC9pGb3QOT67Kn3RlUdbXpo3uvbDKfPU8EgytS4RVaPmDjhqDjj8AGaiz9mk/ppd2Q2WS+gw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.80.0.tgz", + "integrity": "sha512-n8pkXQxuo5zCWXX5cbSNZj1vuWS8IVNGWTmP1m31Iq1k0e8lPZ07PF08TRV79HHq3mEPP/Ko//BQuflHvY2o8w==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.80.0", + "@supabase/functions-js": "2.80.0", + "@supabase/postgrest-js": "2.80.0", + "@supabase/realtime-js": "2.80.0", + "@supabase/storage-js": "2.80.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "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.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "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", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "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", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord.js": { + "version": "14.24.2", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.2.tgz", + "integrity": "sha512-VMEDbmguRdX/EeMaTsf9Mb0IQA90WdYF2cn4QDfslQFXgQ6LFtmlPn0FSotnS0kcFbFp+JBSIxtnF+bnAHG/hQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.1", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "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", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "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", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "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", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "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/attached_assets/bot1/discord-bot/package.json b/attached_assets/bot1/discord-bot/package.json new file mode 100644 index 0000000..b65f0ac --- /dev/null +++ b/attached_assets/bot1/discord-bot/package.json @@ -0,0 +1,34 @@ +{ + "name": "aethex-discord-bot", + "version": "1.0.0", + "description": "AeThex Discord Bot - Account linking, role management, and realm selection", + "main": "bot.js", + "type": "commonjs", + "scripts": { + "start": "node bot.js", + "dev": "nodemon bot.js", + "register-commands": "node scripts/register-commands.js" + }, + "keywords": [ + "discord", + "bot", + "aethex", + "role-management", + "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", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/attached_assets/bot1/discord-bot/scripts/register-commands.js b/attached_assets/bot1/discord-bot/scripts/register-commands.js new file mode 100644 index 0000000..27ffb8d --- /dev/null +++ b/attached_assets/bot1/discord-bot/scripts/register-commands.js @@ -0,0 +1,110 @@ +const { REST, Routes } = require("discord.js"); +const fs = require("fs"); +const path = require("path"); +require("dotenv").config(); + +// Validate environment variables +const requiredEnvVars = ["DISCORD_BOT_TOKEN", "DISCORD_CLIENT_ID"]; + +const missingVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); +if (missingVars.length > 0) { + console.error( + "โŒ FATAL ERROR: Missing required environment variables:", + missingVars.join(", "), + ); + console.error("\nPlease set these before running command registration:"); + missingVars.forEach((envVar) => { + console.error(` - ${envVar}`); + }); + process.exit(1); +} + +// Load commands from commands directory +const commandsPath = path.join(__dirname, "../commands"); +const commandFiles = fs + .readdirSync(commandsPath) + .filter((file) => file.endsWith(".js")); + +const commands = []; + +for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + const command = require(filePath); + if ("data" in command && "execute" in command) { + commands.push(command.data.toJSON()); + console.log(`โœ… Loaded command: ${command.data.name}`); + } +} + +// Register commands with Discord API +async function registerCommands() { + try { + const rest = new REST({ version: "10" }).setToken( + process.env.DISCORD_BOT_TOKEN, + ); + + console.log(`\n๐Ÿ“ Registering ${commands.length} slash commands...`); + console.log( + "โš ๏ธ This will co-exist with Discord's auto-generated Entry Point command.\n", + ); + + try { + const data = await rest.put( + Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), + { body: commands }, + ); + console.log(`โœ… Successfully registered ${data.length} slash commands.`); + console.log("\n๐ŸŽ‰ Command registration complete!"); + console.log("โ„น๏ธ Your commands are now live in Discord."); + console.log( + "โ„น๏ธ The Entry Point command (for Activities) will be managed by Discord.\n", + ); + } catch (error) { + // Handle Entry Point command conflict + if (error.code === 50240) { + console.warn( + "โš ๏ธ Error 50240: Entry Point command detected (Discord Activity enabled).", + ); + console.warn("Registering commands individually...\n"); + + let successCount = 0; + for (const command of commands) { + try { + await rest.post( + Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), + { body: command }, + ); + successCount++; + } catch (postError) { + if (postError.code === 50045) { + console.warn( + ` โš ๏ธ ${command.name}: Already registered (skipping)`, + ); + } else { + console.error(` โŒ ${command.name}: ${postError.message}`); + } + } + } + + console.log( + `\nโœ… Registered ${successCount} slash commands (individual mode).`, + ); + console.log("๐ŸŽ‰ Command registration complete!"); + console.log( + "โ„น๏ธ The Entry Point command will be managed by Discord.\n", + ); + } else { + throw error; + } + } + } catch (error) { + console.error( + "โŒ Fatal error registering commands:", + error.message || error, + ); + process.exit(1); + } +} + +// Run registration +registerCommands(); diff --git a/attached_assets/bot1/discord-bot/utils/roleManager.js b/attached_assets/bot1/discord-bot/utils/roleManager.js new file mode 100644 index 0000000..27be439 --- /dev/null +++ b/attached_assets/bot1/discord-bot/utils/roleManager.js @@ -0,0 +1,137 @@ +const { EmbedBuilder } = require("discord.js"); + +/** + * Assign Discord role based on user's arm and type + * @param {Guild} guild - Discord guild + * @param {string} discordId - Discord user ID + * @param {string} arm - User's primary arm (labs, gameforge, corp, foundation, devlink) + * @param {object} supabase - Supabase client + * @returns {Promise} - Success status + */ +async function assignRoleByArm(guild, discordId, arm, supabase) { + try { + // Fetch guild member + const member = await guild.members.fetch(discordId); + if (!member) { + console.warn(`Member not found: ${discordId}`); + return false; + } + + // Get role mapping from Supabase + const { data: mapping, error: mapError } = await supabase + .from("discord_role_mappings") + .select("discord_role") + .eq("arm", arm) + .eq("server_id", guild.id) + .maybeSingle(); + + if (mapError) { + console.error("Error fetching role mapping:", mapError); + return false; + } + + if (!mapping) { + console.warn( + `No role mapping found for arm: ${arm} in server: ${guild.id}`, + ); + return false; + } + + // Find role by name or ID + let roleToAssign = guild.roles.cache.find( + (r) => r.id === mapping.discord_role || r.name === mapping.discord_role, + ); + + if (!roleToAssign) { + console.warn(`Role not found: ${mapping.discord_role}`); + return false; + } + + // Remove old arm roles + const armRoles = member.roles.cache.filter((role) => + ["Labs", "GameForge", "Corp", "Foundation", "Dev-Link"].some((arm) => + role.name.includes(arm), + ), + ); + + for (const [, role] of armRoles) { + try { + if (role.id !== roleToAssign.id) { + await member.roles.remove(role); + } + } catch (e) { + console.warn(`Could not remove role ${role.name}: ${e.message}`); + } + } + + // Assign new role + if (!member.roles.cache.has(roleToAssign.id)) { + await member.roles.add(roleToAssign); + console.log( + `โœ… Assigned role ${roleToAssign.name} to ${member.user.tag}`, + ); + return true; + } + + return true; + } catch (error) { + console.error("Error assigning role:", error); + return false; + } +} + +/** + * Get user's primary arm from Supabase + * @param {string} discordId - Discord user ID + * @param {object} supabase - Supabase client + * @returns {Promise} - Primary arm (labs, gameforge, corp, foundation, devlink) + */ +async function getUserArm(discordId, supabase) { + try { + const { data: link, error } = await supabase + .from("discord_links") + .select("primary_arm") + .eq("discord_id", discordId) + .maybeSingle(); + + if (error) { + console.error("Error fetching user arm:", error); + return null; + } + + return link?.primary_arm || null; + } catch (error) { + console.error("Error getting user arm:", error); + return null; + } +} + +/** + * Sync roles for a user across all guilds + * @param {Client} client - Discord client + * @param {string} discordId - Discord user ID + * @param {string} arm - Primary arm + * @param {object} supabase - Supabase client + */ +async function syncRolesAcrossGuilds(client, discordId, arm, supabase) { + try { + for (const [, guild] of client.guilds.cache) { + try { + const member = await guild.members.fetch(discordId); + if (member) { + await assignRoleByArm(guild, discordId, arm, supabase); + } + } catch (e) { + console.warn(`Could not sync roles in guild ${guild.id}: ${e.message}`); + } + } + } catch (error) { + console.error("Error syncing roles across guilds:", error); + } +} + +module.exports = { + assignRoleByArm, + getUserArm, + syncRolesAcrossGuilds, +}; diff --git a/attached_assets/bot2/discord-bot/.env.example b/attached_assets/bot2/discord-bot/.env.example new file mode 100644 index 0000000..a4ddc91 --- /dev/null +++ b/attached_assets/bot2/discord-bot/.env.example @@ -0,0 +1,23 @@ +# Discord Bot Configuration +DISCORD_BOT_TOKEN=your_bot_token_here +DISCORD_CLIENT_ID=your_client_id_here +DISCORD_PUBLIC_KEY=your_public_key_here + +# Supabase Configuration +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_SERVICE_ROLE=your_service_role_key_here + +# 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 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 diff --git a/attached_assets/bot2/discord-bot/DEPLOYMENT_GUIDE.md b/attached_assets/bot2/discord-bot/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..c90a3aa --- /dev/null +++ b/attached_assets/bot2/discord-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/attached_assets/bot2/discord-bot/Dockerfile b/attached_assets/bot2/discord-bot/Dockerfile new file mode 100644 index 0000000..279b52f --- /dev/null +++ b/attached_assets/bot2/discord-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/attached_assets/bot2/discord-bot/bot.js b/attached_assets/bot2/discord-bot/bot.js new file mode 100644 index 0000000..b7b2850 --- /dev/null +++ b/attached_assets/bot2/discord-bot/bot.js @@ -0,0 +1,803 @@ +const { + Client, + GatewayIntentBits, + REST, + Routes, + Collection, + EmbedBuilder, +} = require("discord.js"); +const { createClient } = require("@supabase/supabase-js"); +const http = require("http"); +const fs = require("fs"); +const path = require("path"); +require("dotenv").config(); + +const { setupFeedListener, sendPostToDiscord, getFeedChannelId } = require("./listeners/feedSync"); + +// Validate environment variables +const requiredEnvVars = [ + "DISCORD_BOT_TOKEN", + "DISCORD_CLIENT_ID", + "SUPABASE_URL", + "SUPABASE_SERVICE_ROLE", +]; + +const missingVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); +if (missingVars.length > 0) { + console.error( + "โŒ FATAL ERROR: Missing required environment variables:", + missingVars.join(", "), + ); + console.error("\nPlease set these in your Discloud/hosting environment:"); + missingVars.forEach((envVar) => { + console.error(` - ${envVar}`); + }); + process.exit(1); +} + +// Validate token format +const token = process.env.DISCORD_BOT_TOKEN; +if (!token || token.length < 20) { + console.error("โŒ FATAL ERROR: DISCORD_BOT_TOKEN is empty or invalid"); + console.error(` Length: ${token ? token.length : 0}`); + process.exit(1); +} + +console.log("[Token] Bot token loaded (length: " + token.length + " chars)"); + +// Initialize Discord client with message intents for feed sync +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.DirectMessages, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], +}); + +// Initialize Supabase +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE, +); + +// Store slash commands +client.commands = new Collection(); + +// Load commands from commands directory +const commandsPath = path.join(__dirname, "commands"); +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}`); + } +} + +// Load event handlers from events directory +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 listener: ${event.name}`); + } + } +} + +// Slash command interaction handler +client.on("interactionCreate", async (interaction) => { + if (!interaction.isChatInputCommand()) return; + + const command = client.commands.get(interaction.commandName); + + if (!command) { + console.warn( + `โš ๏ธ No command matching ${interaction.commandName} was found.`, + ); + return; + } + + try { + await command.execute(interaction, supabase, client); + } catch (error) { + console.error(`โŒ Error executing ${interaction.commandName}:`, error); + + const errorEmbed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Command Error") + .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 }); + } else { + await interaction.reply({ embeds: [errorEmbed], ephemeral: true }); + } + } +}); + +// IMPORTANT: Commands are now registered via a separate script +// Run this ONCE during deployment: npm run register-commands +// This prevents Error 50240 (Entry Point conflict) when Activities are enabled +// The bot will simply load and listen for the already-registered commands + +// Define all commands for registration +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, + }, + ], + }, +]; + +// Function to register commands with Discord +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 { + // Try bulk update first + 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) { + // Handle Error 50240 (Entry Point conflict) + 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 }; + } +} + +// Start HTTP health check server +const healthPort = process.env.HEALTH_PORT || 8044; +const ADMIN_TOKEN = process.env.DISCORD_ADMIN_TOKEN || "aethex-bot-admin"; + +// Helper to check admin authentication +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()), + timestamp: new Date().toISOString(), + }), + ); + return; + } + + // GET /bot-status - Comprehensive bot status for management panel (requires auth) + 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, + }, + timestamp: new Date().toISOString(), + }), + ); + return; + } + + // GET /linked-users - Get all Discord-linked users (requires auth, sanitizes PII) + if (req.url === "/linked-users") { + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + 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; + } + + // GET /command-stats - Get command usage statistics (requires auth) + if (req.url === "/command-stats") { + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + return; + } + + (async () => { + try { + 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 })); + } catch (error) { + res.writeHead(500); + res.end(JSON.stringify({ success: false, error: error.message })); + } + })(); + return; + } + + // GET /feed-stats - Get feed bridge statistics (requires auth) + if (req.url === "/feed-stats") { + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + 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; + } + + // POST /send-to-discord - Send a post from AeThex to Discord channel + if (req.url === "/send-to-discord" && req.method === "POST") { + let body = ""; + req.on("data", (chunk) => { + body += chunk.toString(); + }); + req.on("end", async () => { + try { + // Simple auth check + 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); + + const result = await sendPostToDiscord(post, post.author); + res.writeHead(result.success ? 200 : 500); + res.end(JSON.stringify(result)); + } catch (error) { + console.error("[API] Error processing send-to-discord:", error); + res.writeHead(500); + res.end(JSON.stringify({ error: error.message })); + } + }); + return; + } + + // GET /bridge-status - Check if bridge is configured + 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; + } + // Show HTML form with button + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(` + + + + Register Discord Commands + + + +
+

๐Ÿค– Discord Commands Registration

+

Click the button below to register all Discord slash commands (/verify, /set-realm, /profile, /unlink, /verify-role)

+ + + +
โณ Registering... please wait...
+
+
+ + + + + `); + return; + } + + if (req.method === "POST") { + // Verify admin token + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + return; + } + + // Register commands + 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`, + ); + }); + +// Login with error handling +client.login(process.env.DISCORD_BOT_TOKEN).catch((error) => { + console.error("โŒ FATAL ERROR: Failed to login to Discord"); + console.error(` Error Code: ${error.code}`); + console.error(` Error Message: ${error.message}`); + + if (error.code === "TokenInvalid") { + console.error("\nโš ๏ธ DISCORD_BOT_TOKEN is invalid!"); + console.error(" Possible causes:"); + console.error(" 1. Token has been revoked by Discord"); + console.error(" 2. Token has expired"); + console.error(" 3. Token format is incorrect"); + console.error( + "\n Solution: Get a new bot token from Discord Developer Portal", + ); + console.error(" https://discord.com/developers/applications"); + } + + process.exit(1); +}); + +client.once("ready", () => { + console.log(`โœ… Bot logged in as ${client.user.tag}`); + console.log(`๐Ÿ“ก Listening in ${client.guilds.cache.size} server(s)`); + console.log("โ„น๏ธ Commands are registered via: npm run register-commands"); + + // Set bot status + client.user.setActivity("/verify to link your AeThex account", { + type: "LISTENING", + }); + + // Setup bidirectional feed bridge (AeThex โ†’ Discord) + setupFeedListener(client); +}); + +// Error handling +process.on("unhandledRejection", (error) => { + console.error("โŒ Unhandled Promise Rejection:", error); +}); + +process.on("uncaughtException", (error) => { + console.error("โŒ Uncaught Exception:", error); + process.exit(1); +}); + +module.exports = client; diff --git a/attached_assets/bot2/discord-bot/commands/help.js b/attached_assets/bot2/discord-bot/commands/help.js new file mode 100644 index 0000000..324b1dd --- /dev/null +++ b/attached_assets/bot2/discord-bot/commands/help.js @@ -0,0 +1,55 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("help") + .setDescription("View all AeThex bot commands and features"), + + async execute(interaction) { + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("๐Ÿค– AeThex Bot Commands") + .setDescription("Here are all the commands you can use with the AeThex Discord bot.") + .addFields( + { + name: "๐Ÿ”— Account Linking", + value: [ + "`/verify` - Link your Discord account to AeThex", + "`/unlink` - Disconnect your Discord from AeThex", + "`/profile` - View your linked AeThex profile", + ].join("\n"), + }, + { + name: "โš”๏ธ Realm Management", + value: [ + "`/set-realm` - Choose your primary realm (Labs, GameForge, Corp, Foundation, Dev-Link)", + "`/verify-role` - Check your assigned Discord roles", + ].join("\n"), + }, + { + name: "๐Ÿ“Š Community", + value: [ + "`/stats` - View your AeThex statistics and activity", + "`/leaderboard` - See the top contributors", + "`/post` - Create a post in the AeThex community feed", + ].join("\n"), + }, + { + name: "โ„น๏ธ Information", + value: "`/help` - Show this help message", + }, + ) + .addFields({ + name: "๐Ÿ”— Quick Links", + value: [ + "[AeThex Platform](https://aethex.dev)", + "[Creator Directory](https://aethex.dev/creators)", + "[Community Feed](https://aethex.dev/community/feed)", + ].join(" | "), + }) + .setFooter({ text: "AeThex | Build. Create. Connect." }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + }, +}; diff --git a/attached_assets/bot2/discord-bot/commands/leaderboard.js b/attached_assets/bot2/discord-bot/commands/leaderboard.js new file mode 100644 index 0000000..cbc5b01 --- /dev/null +++ b/attached_assets/bot2/discord-bot/commands/leaderboard.js @@ -0,0 +1,155 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("leaderboard") + .setDescription("View the top AeThex contributors") + .addStringOption((option) => + option + .setName("category") + .setDescription("Leaderboard category") + .setRequired(false) + .addChoices( + { name: "๐Ÿ”ฅ Most Active (Posts)", value: "posts" }, + { name: "โค๏ธ Most Liked", value: "likes" }, + { name: "๐ŸŽจ Top Creators", value: "creators" } + ) + ), + + async execute(interaction, supabase) { + await interaction.deferReply(); + + try { + const category = interaction.options.getString("category") || "posts"; + + let leaderboardData = []; + let title = ""; + let emoji = ""; + + if (category === "posts") { + title = "Most Active Posters"; + emoji = "๐Ÿ”ฅ"; + + const { data: posts } = await supabase + .from("community_posts") + .select("user_id") + .not("user_id", "is", null); + + const postCounts = {}; + posts?.forEach((post) => { + postCounts[post.user_id] = (postCounts[post.user_id] || 0) + 1; + }); + + const sortedUsers = Object.entries(postCounts) + .sort(([, a], [, b]) => b - a) + .slice(0, 10); + + for (const [userId, count] of sortedUsers) { + const { data: profile } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", userId) + .single(); + + if (profile) { + leaderboardData.push({ + name: profile.full_name || profile.username || "Anonymous", + value: `${count} posts`, + username: profile.username, + }); + } + } + } else if (category === "likes") { + title = "Most Liked Users"; + emoji = "โค๏ธ"; + + const { data: posts } = await supabase + .from("community_posts") + .select("user_id, likes_count") + .not("user_id", "is", null) + .order("likes_count", { ascending: false }); + + const likeCounts = {}; + posts?.forEach((post) => { + likeCounts[post.user_id] = + (likeCounts[post.user_id] || 0) + (post.likes_count || 0); + }); + + const sortedUsers = Object.entries(likeCounts) + .sort(([, a], [, b]) => b - a) + .slice(0, 10); + + for (const [userId, count] of sortedUsers) { + const { data: profile } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", userId) + .single(); + + if (profile) { + leaderboardData.push({ + name: profile.full_name || profile.username || "Anonymous", + value: `${count} likes received`, + username: profile.username, + }); + } + } + } else if (category === "creators") { + title = "Top Creators"; + emoji = "๐ŸŽจ"; + + const { data: creators } = await supabase + .from("aethex_creators") + .select("user_id, total_projects, verified, featured") + .order("total_projects", { ascending: false }) + .limit(10); + + for (const creator of creators || []) { + const { data: profile } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", creator.user_id) + .single(); + + if (profile) { + const badges = []; + if (creator.verified) badges.push("โœ…"); + if (creator.featured) badges.push("โญ"); + + leaderboardData.push({ + name: profile.full_name || profile.username || "Anonymous", + value: `${creator.total_projects || 0} projects ${badges.join(" ")}`, + username: profile.username, + }); + } + } + } + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle(`${emoji} ${title}`) + .setDescription( + leaderboardData.length > 0 + ? leaderboardData + .map( + (user, index) => + `**${index + 1}.** ${user.name} - ${user.value}` + ) + .join("\n") + : "No data available yet. Be the first to contribute!" + ) + .setFooter({ text: "AeThex Leaderboard | Updated in real-time" }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Leaderboard command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to fetch leaderboard. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot2/discord-bot/commands/post.js b/attached_assets/bot2/discord-bot/commands/post.js new file mode 100644 index 0000000..61057e6 --- /dev/null +++ b/attached_assets/bot2/discord-bot/commands/post.js @@ -0,0 +1,144 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + ActionRowBuilder, +} = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("post") + .setDescription("Create a post in the AeThex community feed") + .addStringOption((option) => + option + .setName("content") + .setDescription("Your post content") + .setRequired(true) + .setMaxLength(500) + ) + .addStringOption((option) => + option + .setName("category") + .setDescription("Post category") + .setRequired(false) + .addChoices( + { name: "๐Ÿ’ฌ General", value: "general" }, + { name: "๐Ÿš€ Project Update", value: "project_update" }, + { name: "โ“ Question", value: "question" }, + { name: "๐Ÿ’ก Idea", value: "idea" }, + { name: "๐ŸŽ‰ Announcement", value: "announcement" } + ) + ) + .addAttachmentOption((option) => + option + .setName("image") + .setDescription("Attach an image to your post") + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started." + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", link.user_id) + .single(); + + const content = interaction.options.getString("content"); + const category = interaction.options.getString("category") || "general"; + const attachment = interaction.options.getAttachment("image"); + + let imageUrl = null; + if (attachment && attachment.contentType?.startsWith("image/")) { + imageUrl = attachment.url; + } + + const categoryLabels = { + general: "General", + project_update: "Project Update", + question: "Question", + idea: "Idea", + announcement: "Announcement", + }; + + const { data: post, error } = await supabase + .from("community_posts") + .insert({ + user_id: link.user_id, + content: content, + category: category, + arm_affiliation: link.primary_arm || "general", + image_url: imageUrl, + source: "discord", + discord_message_id: interaction.id, + discord_author_id: interaction.user.id, + discord_author_name: interaction.user.username, + discord_author_avatar: interaction.user.displayAvatarURL(), + }) + .select() + .single(); + + if (error) throw error; + + const successEmbed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Post Created!") + .setDescription(content.length > 100 ? content.slice(0, 100) + "..." : content) + .addFields( + { + name: "๐Ÿ“ Category", + value: categoryLabels[category], + inline: true, + }, + { + name: "โš”๏ธ Realm", + value: link.primary_arm || "general", + inline: true, + } + ); + + if (imageUrl) { + successEmbed.setImage(imageUrl); + } + + successEmbed + .addFields({ + name: "๐Ÿ”— View Post", + value: `[Open in AeThex](https://aethex.dev/community/feed)`, + }) + .setFooter({ text: "Your post is now live on AeThex!" }) + .setTimestamp(); + + await interaction.editReply({ embeds: [successEmbed] }); + } catch (error) { + console.error("Post command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to create post. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot2/discord-bot/commands/profile.js b/attached_assets/bot2/discord-bot/commands/profile.js new file mode 100644 index 0000000..035f251 --- /dev/null +++ b/attached_assets/bot2/discord-bot/commands/profile.js @@ -0,0 +1,93 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("profile") + .setDescription("View your AeThex profile in Discord"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("*") + .eq("id", link.user_id) + .single(); + + if (!profile) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Profile Not Found") + .setDescription("Your AeThex profile could not be found."); + + return await interaction.editReply({ embeds: [embed] }); + } + + const armEmojis = { + labs: "๐Ÿงช", + gameforge: "๐ŸŽฎ", + corp: "๐Ÿ’ผ", + foundation: "๐Ÿค", + devlink: "๐Ÿ’ป", + }; + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle(`${profile.full_name || "AeThex User"}'s Profile`) + .setThumbnail( + profile.avatar_url || "https://aethex.dev/placeholder.svg", + ) + .addFields( + { + name: "๐Ÿ‘ค Username", + value: profile.username || "N/A", + inline: true, + }, + { + name: `${armEmojis[link.primary_arm] || "โš”๏ธ"} Primary Realm`, + value: link.primary_arm || "Not set", + inline: true, + }, + { + name: "๐Ÿ“Š Role", + value: profile.user_type || "community_member", + inline: true, + }, + { name: "๐Ÿ“ Bio", value: profile.bio || "No bio set", inline: false }, + ) + .addFields({ + name: "๐Ÿ”— Links", + value: `[Visit Full Profile](https://aethex.dev/creators/${profile.username})`, + }) + .setFooter({ text: "AeThex | Your Web3 Creator Hub" }); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Profile command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to fetch profile. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot2/discord-bot/commands/refresh-roles.js b/attached_assets/bot2/discord-bot/commands/refresh-roles.js new file mode 100644 index 0000000..459bd79 --- /dev/null +++ b/attached_assets/bot2/discord-bot/commands/refresh-roles.js @@ -0,0 +1,72 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const { assignRoleByArm, getUserArm } = require("../utils/roleManager"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("refresh-roles") + .setDescription( + "Refresh your Discord roles based on your current AeThex settings", + ), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + // Check if user is linked + const { data: link } = await supabase + .from("discord_links") + .select("primary_arm") + .eq("discord_id", interaction.user.id) + .maybeSingle(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + if (!link.primary_arm) { + const embed = new EmbedBuilder() + .setColor(0xffaa00) + .setTitle("โš ๏ธ No Realm Set") + .setDescription( + "You haven't set your primary realm yet.\nUse `/set-realm` to choose one.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + // Assign role based on current primary arm + const roleAssigned = await assignRoleByArm( + interaction.guild, + interaction.user.id, + link.primary_arm, + supabase, + ); + + const embed = new EmbedBuilder() + .setColor(roleAssigned ? 0x00ff00 : 0xffaa00) + .setTitle("โœ… Roles Refreshed") + .setDescription( + roleAssigned + ? `Your Discord roles have been synced with your AeThex account.\n\nPrimary Realm: **${link.primary_arm}**` + : `Your roles could not be automatically assigned.\n\nPrimary Realm: **${link.primary_arm}**\n\nโš ๏ธ Please contact an admin to set up the role mapping for this server.`, + ); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Refresh-roles command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to refresh roles. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot2/discord-bot/commands/set-realm.js b/attached_assets/bot2/discord-bot/commands/set-realm.js new file mode 100644 index 0000000..c1af120 --- /dev/null +++ b/attached_assets/bot2/discord-bot/commands/set-realm.js @@ -0,0 +1,139 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + StringSelectMenuBuilder, + ActionRowBuilder, +} = require("discord.js"); +const { assignRoleByArm } = require("../utils/roleManager"); + +const REALMS = [ + { value: "labs", label: "๐Ÿงช Labs", description: "Research & Development" }, + { + value: "gameforge", + label: "๐ŸŽฎ GameForge", + description: "Game Development", + }, + { value: "corp", label: "๐Ÿ’ผ Corp", description: "Enterprise Solutions" }, + { + value: "foundation", + label: "๐Ÿค Foundation", + description: "Community & Education", + }, + { + value: "devlink", + label: "๐Ÿ’ป Dev-Link", + description: "Professional Networking", + }, +]; + +module.exports = { + data: new SlashCommandBuilder() + .setName("set-realm") + .setDescription("Set your primary AeThex realm/arm"), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const select = new StringSelectMenuBuilder() + .setCustomId("select_realm") + .setPlaceholder("Choose your primary realm") + .addOptions( + REALMS.map((realm) => ({ + label: realm.label, + description: realm.description, + value: realm.value, + default: realm.value === link.primary_arm, + })), + ); + + const row = new ActionRowBuilder().addComponents(select); + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("โš”๏ธ Choose Your Realm") + .setDescription( + "Select your primary AeThex realm. This determines your main Discord role.", + ) + .addFields({ + name: "Current Realm", + value: link.primary_arm || "Not set", + }); + + await interaction.editReply({ embeds: [embed], components: [row] }); + + const filter = (i) => + i.user.id === interaction.user.id && i.customId === "select_realm"; + const collector = interaction.channel.createMessageComponentCollector({ + filter, + time: 60000, + }); + + collector.on("collect", async (i) => { + const selectedRealm = i.values[0]; + + await supabase + .from("discord_links") + .update({ primary_arm: selectedRealm }) + .eq("discord_id", interaction.user.id); + + const realm = REALMS.find((r) => r.value === selectedRealm); + + // Assign Discord role based on selected realm + const roleAssigned = await assignRoleByArm( + interaction.guild, + interaction.user.id, + selectedRealm, + supabase, + ); + + const roleStatus = roleAssigned + ? "โœ… Discord role assigned!" + : "โš ๏ธ No role mapping found for this realm in this server."; + + const confirmEmbed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Realm Set") + .setDescription( + `Your primary realm is now **${realm.label}**\n\n${roleStatus}`, + ); + + await i.update({ embeds: [confirmEmbed], components: [] }); + }); + + collector.on("end", (collected) => { + if (collected.size === 0) { + interaction.editReply({ + content: "Realm selection timed out.", + components: [], + }); + } + }); + } catch (error) { + console.error("Set-realm command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to update realm. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot2/discord-bot/commands/stats.js b/attached_assets/bot2/discord-bot/commands/stats.js new file mode 100644 index 0000000..fe9814b --- /dev/null +++ b/attached_assets/bot2/discord-bot/commands/stats.js @@ -0,0 +1,140 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("stats") + .setDescription("View your AeThex statistics and activity"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm, created_at") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started." + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("*") + .eq("id", link.user_id) + .single(); + + const { count: postCount } = await supabase + .from("community_posts") + .select("*", { count: "exact", head: true }) + .eq("user_id", link.user_id); + + const { count: likeCount } = await supabase + .from("community_likes") + .select("*", { count: "exact", head: true }) + .eq("user_id", link.user_id); + + const { count: commentCount } = await supabase + .from("community_comments") + .select("*", { count: "exact", head: true }) + .eq("user_id", link.user_id); + + const { data: creatorProfile } = await supabase + .from("aethex_creators") + .select("verified, featured, total_projects") + .eq("user_id", link.user_id) + .single(); + + const armEmojis = { + labs: "๐Ÿงช", + gameforge: "๐ŸŽฎ", + corp: "๐Ÿ’ผ", + foundation: "๐Ÿค", + devlink: "๐Ÿ’ป", + }; + + const linkedDate = new Date(link.created_at); + const daysSinceLinked = Math.floor( + (Date.now() - linkedDate.getTime()) / (1000 * 60 * 60 * 24) + ); + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle(`๐Ÿ“Š ${profile?.full_name || interaction.user.username}'s Stats`) + .setThumbnail(profile?.avatar_url || interaction.user.displayAvatarURL()) + .addFields( + { + name: `${armEmojis[link.primary_arm] || "โš”๏ธ"} Primary Realm`, + value: link.primary_arm || "Not set", + inline: true, + }, + { + name: "๐Ÿ‘ค Account Type", + value: profile?.user_type || "community_member", + inline: true, + }, + { + name: "๐Ÿ“… Days Linked", + value: `${daysSinceLinked} days`, + inline: true, + } + ) + .addFields( + { + name: "๐Ÿ“ Posts", + value: `${postCount || 0}`, + inline: true, + }, + { + name: "โค๏ธ Likes Given", + value: `${likeCount || 0}`, + inline: true, + }, + { + name: "๐Ÿ’ฌ Comments", + value: `${commentCount || 0}`, + inline: true, + } + ); + + if (creatorProfile) { + embed.addFields({ + name: "๐ŸŽจ Creator Status", + value: [ + creatorProfile.verified ? "โœ… Verified Creator" : "โณ Pending Verification", + creatorProfile.featured ? "โญ Featured" : "", + `๐Ÿ“ ${creatorProfile.total_projects || 0} Projects`, + ] + .filter(Boolean) + .join("\n"), + }); + } + + embed + .addFields({ + name: "๐Ÿ”— Full Profile", + value: `[View on AeThex](https://aethex.dev/creators/${profile?.username || link.user_id})`, + }) + .setFooter({ text: "AeThex | Your Creative Hub" }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Stats command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to fetch stats. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot2/discord-bot/commands/unlink.js b/attached_assets/bot2/discord-bot/commands/unlink.js new file mode 100644 index 0000000..ac06d2a --- /dev/null +++ b/attached_assets/bot2/discord-bot/commands/unlink.js @@ -0,0 +1,75 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("unlink") + .setDescription("Unlink your Discord account from AeThex"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("*") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โ„น๏ธ Not Linked") + .setDescription("Your Discord account is not linked to AeThex."); + + return await interaction.editReply({ embeds: [embed] }); + } + + // Delete the link + await supabase + .from("discord_links") + .delete() + .eq("discord_id", interaction.user.id); + + // Remove Discord roles from user + const guild = interaction.guild; + const member = await guild.members.fetch(interaction.user.id); + + // Find and remove all AeThex-related roles + const rolesToRemove = member.roles.cache.filter( + (role) => + role.name.includes("Labs") || + role.name.includes("GameForge") || + role.name.includes("Corp") || + role.name.includes("Foundation") || + role.name.includes("Dev-Link") || + role.name.includes("Premium") || + role.name.includes("Creator"), + ); + + for (const [, role] of rolesToRemove) { + try { + await member.roles.remove(role); + } catch (e) { + console.warn(`Could not remove role ${role.name}`); + } + } + + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Account Unlinked") + .setDescription( + "Your Discord account has been unlinked from AeThex.\nAll associated roles have been removed.", + ); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Unlink command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to unlink account. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot2/discord-bot/commands/verify-role.js b/attached_assets/bot2/discord-bot/commands/verify-role.js new file mode 100644 index 0000000..1b7e6b9 --- /dev/null +++ b/attached_assets/bot2/discord-bot/commands/verify-role.js @@ -0,0 +1,97 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("verify-role") + .setDescription("Check your AeThex-assigned Discord roles"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("user_type") + .eq("id", link.user_id) + .single(); + + const { data: mappings } = await supabase + .from("discord_role_mappings") + .select("discord_role") + .eq("arm", link.primary_arm) + .eq("user_type", profile?.user_type || "community_member"); + + const member = await interaction.guild.members.fetch(interaction.user.id); + const aethexRoles = member.roles.cache.filter( + (role) => + role.name.includes("Labs") || + role.name.includes("GameForge") || + role.name.includes("Corp") || + role.name.includes("Foundation") || + role.name.includes("Dev-Link") || + role.name.includes("Premium") || + role.name.includes("Creator"), + ); + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("๐Ÿ” Your AeThex Roles") + .addFields( + { + name: "โš”๏ธ Primary Realm", + value: link.primary_arm || "Not set", + inline: true, + }, + { + name: "๐Ÿ‘ค User Type", + value: profile?.user_type || "community_member", + inline: true, + }, + { + name: "๐ŸŽญ Discord Roles", + value: + aethexRoles.size > 0 + ? aethexRoles.map((r) => r.name).join(", ") + : "None assigned yet", + }, + { + name: "๐Ÿ“‹ Expected Roles", + value: + mappings?.length > 0 + ? mappings.map((m) => m.discord_role).join(", ") + : "No mappings found", + }, + ) + .setFooter({ + text: "Roles are assigned automatically based on your AeThex profile", + }); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Verify-role command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to verify roles. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot2/discord-bot/commands/verify.js b/attached_assets/bot2/discord-bot/commands/verify.js new file mode 100644 index 0000000..d9f30e7 --- /dev/null +++ b/attached_assets/bot2/discord-bot/commands/verify.js @@ -0,0 +1,85 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, +} = require("discord.js"); +const { syncRolesAcrossGuilds } = require("../utils/roleManager"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("verify") + .setDescription("Link your Discord account to your AeThex account"), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: existingLink } = await supabase + .from("discord_links") + .select("*") + .eq("discord_id", interaction.user.id) + .single(); + + if (existingLink) { + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Already Linked") + .setDescription( + `Your Discord account is already linked to AeThex (User ID: ${existingLink.user_id})`, + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + // Generate verification code + const verificationCode = Math.random() + .toString(36) + .substring(2, 8) + .toUpperCase(); + const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes + + // Store verification code with Discord username + await supabase.from("discord_verifications").insert({ + discord_id: interaction.user.id, + verification_code: verificationCode, + username: interaction.user.username, + expires_at: expiresAt.toISOString(), + }); + + const verifyUrl = `https://aethex.dev/discord-verify?code=${verificationCode}`; + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("๐Ÿ”— Link Your AeThex Account") + .setDescription( + "Click the button below to link your Discord account to AeThex.", + ) + .addFields( + { name: "โฑ๏ธ Expires In", value: "15 minutes" }, + { name: "๐Ÿ“ Verification Code", value: `\`${verificationCode}\`` }, + ) + .setFooter({ text: "Your security code will expire in 15 minutes" }); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("Link Account") + .setStyle(ButtonStyle.Link) + .setURL(verifyUrl), + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); + } catch (error) { + console.error("Verify command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription( + "Failed to generate verification code. Please try again.", + ); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/bot2/discord-bot/discloud.config b/attached_assets/bot2/discord-bot/discloud.config new file mode 100644 index 0000000..fe114e6 --- /dev/null +++ b/attached_assets/bot2/discord-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/attached_assets/bot2/discord-bot/events/messageCreate.js b/attached_assets/bot2/discord-bot/events/messageCreate.js new file mode 100644 index 0000000..75abc33 --- /dev/null +++ b/attached_assets/bot2/discord-bot/events/messageCreate.js @@ -0,0 +1,180 @@ +const { createClient } = require("@supabase/supabase-js"); + +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE, +); + +// Only sync messages from this specific channel +const FEED_CHANNEL_ID = process.env.DISCORD_MAIN_CHAT_CHANNELS + ? process.env.DISCORD_MAIN_CHAT_CHANNELS.split(",")[0].trim() + : null; + +function getArmAffiliation(message) { + const guildName = message.guild?.name?.toLowerCase() || ""; + const channelName = message.channel?.name?.toLowerCase() || ""; + const searchString = `${guildName} ${channelName}`; + + if (searchString.includes("gameforge")) return "gameforge"; + if (searchString.includes("corp")) return "corp"; + if (searchString.includes("foundation")) return "foundation"; + if (searchString.includes("devlink") || searchString.includes("dev-link")) + return "devlink"; + if (searchString.includes("nexus")) return "nexus"; + if (searchString.includes("staff")) return "staff"; + + return "labs"; +} + +async function syncMessageToFeed(message) { + try { + console.log( + `[Feed Sync] Processing from ${message.author.tag} in #${message.channel.name}`, + ); + + const { data: linkedAccount } = await supabase + .from("discord_links") + .select("user_id") + .eq("discord_id", message.author.id) + .single(); + + let authorId = linkedAccount?.user_id; + let authorInfo = null; + + if (authorId) { + const { data: profile } = await supabase + .from("user_profiles") + .select("id, username, full_name, avatar_url") + .eq("id", authorId) + .single(); + authorInfo = profile; + } + + if (!authorId) { + const discordUsername = `discord-${message.author.id}`; + let { data: guestProfile } = await supabase + .from("user_profiles") + .select("id, username, full_name, avatar_url") + .eq("username", discordUsername) + .single(); + + if (!guestProfile) { + const { data: newProfile, error: createError } = await supabase + .from("user_profiles") + .insert({ + username: discordUsername, + full_name: message.author.displayName || message.author.username, + avatar_url: message.author.displayAvatarURL({ size: 256 }), + }) + .select("id, username, full_name, avatar_url") + .single(); + + if (createError) { + console.error("[Feed Sync] Could not create guest profile:", createError); + return; + } + guestProfile = newProfile; + } + + authorId = guestProfile?.id; + authorInfo = guestProfile; + } + + if (!authorId) { + console.error("[Feed Sync] Could not get author ID"); + return; + } + + let content = message.content || "Shared a message on Discord"; + let mediaUrl = null; + let mediaType = "none"; + + if (message.attachments.size > 0) { + const attachment = message.attachments.first(); + if (attachment) { + mediaUrl = attachment.url; + const attachmentLower = attachment.name.toLowerCase(); + + if ( + [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) => + attachmentLower.endsWith(ext), + ) + ) { + mediaType = "image"; + } else if ( + [".mp4", ".webm", ".mov", ".avi"].some((ext) => + attachmentLower.endsWith(ext), + ) + ) { + mediaType = "video"; + } + } + } + + const armAffiliation = getArmAffiliation(message); + + const postContent = JSON.stringify({ + text: content, + mediaUrl: mediaUrl, + mediaType: mediaType, + source: "discord", + discord_message_id: message.id, + discord_channel_id: message.channelId, + discord_channel_name: message.channel.name, + discord_guild_id: message.guildId, + discord_guild_name: message.guild?.name, + discord_author_id: message.author.id, + discord_author_tag: message.author.tag, + discord_author_avatar: message.author.displayAvatarURL({ size: 256 }), + is_linked_user: !!linkedAccount, + }); + + const { error: insertError } = await supabase + .from("community_posts") + .insert({ + title: content.substring(0, 100) || "Discord Message", + content: postContent, + arm_affiliation: armAffiliation, + author_id: authorId, + tags: ["discord", "feed"], + category: "discord", + is_published: true, + likes_count: 0, + comments_count: 0, + }); + + if (insertError) { + console.error("[Feed Sync] Post creation failed:", insertError); + return; + } + + console.log( + `[Feed Sync] โœ… Synced message from ${message.author.tag} to AeThex feed`, + ); + } catch (error) { + console.error("[Feed Sync] Error:", error); + } +} + +module.exports = { + name: "messageCreate", + async execute(message, client) { + // Ignore bot messages + if (message.author.bot) return; + + // Ignore empty messages + if (!message.content && message.attachments.size === 0) return; + + // Only process messages from the configured feed channel + if (!FEED_CHANNEL_ID) { + return; // No channel configured + } + + if (message.channelId !== FEED_CHANNEL_ID) { + return; // Not the feed channel + } + + // Sync this message to AeThex feed + await syncMessageToFeed(message); + }, +}; diff --git a/attached_assets/bot2/discord-bot/listeners/feedSync.js b/attached_assets/bot2/discord-bot/listeners/feedSync.js new file mode 100644 index 0000000..b8168c4 --- /dev/null +++ b/attached_assets/bot2/discord-bot/listeners/feedSync.js @@ -0,0 +1,239 @@ +const { EmbedBuilder } = require("discord.js"); +const { createClient } = require("@supabase/supabase-js"); + +const 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() + : null; + +const POLL_INTERVAL = 5000; // Check every 5 seconds + +let discordClient = null; +let lastCheckedTime = null; +let pollInterval = null; +let isPolling = false; // Concurrency lock to prevent overlapping polls +const processedPostIds = new Set(); // Track already-processed posts to prevent duplicates + +function getArmColor(arm) { + const colors = { + labs: 0x00d4ff, + gameforge: 0xff6b00, + corp: 0x9945ff, + foundation: 0x14f195, + devlink: 0xf7931a, + nexus: 0xff00ff, + staff: 0xffd700, + }; + return colors[arm] || 0x5865f2; +} + +function getArmEmoji(arm) { + const emojis = { + labs: "๐Ÿ”ฌ", + gameforge: "๐ŸŽฎ", + corp: "๐Ÿข", + foundation: "๐ŸŽ“", + devlink: "๐Ÿ”—", + nexus: "๐ŸŒ", + staff: "โญ", + }; + return emojis[arm] || "๐Ÿ“"; +} + +async function sendPostToDiscord(post, authorInfo = null) { + if (!discordClient || !FEED_CHANNEL_ID) { + console.log("[Feed Bridge] No Discord client or channel configured"); + return { success: false, error: "No Discord client or channel configured" }; + } + + try { + const channel = await discordClient.channels.fetch(FEED_CHANNEL_ID); + if (!channel || !channel.isTextBased()) { + console.error("[Feed Bridge] Could not find text channel:", FEED_CHANNEL_ID); + return { success: false, error: "Could not find text channel" }; + } + + let content = {}; + try { + content = typeof post.content === "string" ? JSON.parse(post.content) : post.content; + } catch { + content = { text: post.content }; + } + + if (content.source === "discord") { + return { success: true, skipped: true, reason: "Discord-sourced post" }; + } + + let author = authorInfo; + if (!author && post.author_id) { + const { data } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", post.author_id) + .single(); + author = data; + } + + const authorName = author?.full_name || author?.username || "AeThex User"; + // Discord only accepts HTTP/HTTPS URLs for icons - filter out base64/data URLs + const rawAvatar = author?.avatar_url || ""; + const authorAvatar = rawAvatar.startsWith("http://") || rawAvatar.startsWith("https://") + ? rawAvatar + : "https://aethex.dev/logo.png"; + const arm = post.arm_affiliation || "labs"; + + const embed = new EmbedBuilder() + .setColor(getArmColor(arm)) + .setAuthor({ + name: `${getArmEmoji(arm)} ${authorName}`, + iconURL: authorAvatar, + url: `https://aethex.dev/creators/${author?.username || post.author_id}`, + }) + .setDescription(content.text || post.title || "New post") + .setTimestamp(post.created_at ? new Date(post.created_at) : new Date()) + .setFooter({ + text: `Posted from AeThex โ€ข ${arm.charAt(0).toUpperCase() + arm.slice(1)}`, + iconURL: "https://aethex.dev/logo.png", + }); + + if (content.mediaUrl) { + if (content.mediaType === "image") { + embed.setImage(content.mediaUrl); + } else if (content.mediaType === "video") { + embed.addFields({ + name: "๐ŸŽฌ Video", + value: `[Watch Video](${content.mediaUrl})`, + }); + } + } + + if (post.tags && post.tags.length > 0) { + const tagString = post.tags + .filter((t) => t !== "discord" && t !== "main-chat") + .map((t) => `#${t}`) + .join(" "); + if (tagString) { + embed.addFields({ name: "Tags", value: tagString, inline: true }); + } + } + + const postUrl = `https://aethex.dev/community/feed?post=${post.id}`; + embed.addFields({ + name: "๐Ÿ”— View on AeThex", + value: `[Open Post](${postUrl})`, + inline: true, + }); + + await channel.send({ embeds: [embed] }); + console.log(`[Feed Bridge] โœ… Sent post ${post.id} to Discord`); + return { success: true }; + } catch (error) { + console.error("[Feed Bridge] Error sending to Discord:", error); + return { success: false, error: error.message }; + } +} + +async function checkForNewPosts() { + if (!discordClient || !FEED_CHANNEL_ID) return; + + // Prevent overlapping polls - if already polling, skip this run + if (isPolling) { + console.log("[Feed Bridge] Skipping poll - previous poll still in progress"); + return; + } + + isPolling = true; + + try { + const { data: posts, error } = await supabase + .from("community_posts") + .select("*") + .gt("created_at", lastCheckedTime.toISOString()) + .order("created_at", { ascending: true }); + + if (error) { + console.error("[Feed Bridge] Error fetching new posts:", error); + return; + } + + if (posts && posts.length > 0) { + // Update lastCheckedTime IMMEDIATELY after fetching to prevent re-fetching same posts + lastCheckedTime = new Date(posts[posts.length - 1].created_at); + + // Filter out already-processed posts (double safety) + const newPosts = posts.filter(post => !processedPostIds.has(post.id)); + + if (newPosts.length > 0) { + console.log(`[Feed Bridge] Found ${newPosts.length} new post(s)`); + + for (const post of newPosts) { + // Mark as processed BEFORE sending to prevent duplicates + processedPostIds.add(post.id); + + let content = {}; + try { + content = typeof post.content === "string" ? JSON.parse(post.content) : post.content; + } catch { + content = { text: post.content }; + } + + if (content.source === "discord") { + console.log(`[Feed Bridge] Skipping Discord-sourced post ${post.id}`); + continue; + } + + console.log(`[Feed Bridge] Bridging post ${post.id} to Discord...`); + await sendPostToDiscord(post); + } + } + + // Keep processedPostIds from growing indefinitely - trim old entries + if (processedPostIds.size > 1000) { + const idsArray = Array.from(processedPostIds); + idsArray.slice(0, 500).forEach(id => processedPostIds.delete(id)); + } + } + } catch (error) { + console.error("[Feed Bridge] Poll error:", error); + } finally { + isPolling = false; + } +} + +function setupFeedListener(client) { + discordClient = client; + + if (!FEED_CHANNEL_ID) { + console.log("[Feed Bridge] No DISCORD_MAIN_CHAT_CHANNELS configured - bridge disabled"); + return; + } + + lastCheckedTime = new Date(); + + console.log("[Feed Bridge] Starting polling for new posts (every 5 seconds)..."); + + pollInterval = setInterval(checkForNewPosts, POLL_INTERVAL); + + console.log("[Feed Bridge] โœ… Feed bridge ready (channel: " + FEED_CHANNEL_ID + ")"); +} + +function getDiscordClient() { + return discordClient; +} + +function getFeedChannelId() { + return FEED_CHANNEL_ID; +} + +function cleanup() { + if (pollInterval) { + clearInterval(pollInterval); + console.log("[Feed Bridge] Stopped polling"); + } +} + +module.exports = { setupFeedListener, sendPostToDiscord, getDiscordClient, getFeedChannelId, cleanup }; diff --git a/attached_assets/bot2/discord-bot/package-lock.json b/attached_assets/bot2/discord-bot/package-lock.json new file mode 100644 index 0000000..15b33b8 --- /dev/null +++ b/attached_assets/bot2/discord-bot/package-lock.json @@ -0,0 +1,1157 @@ +{ + "name": "aethex-discord-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aethex-discord-bot", + "version": "1.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", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "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.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.0.tgz", + "integrity": "sha512-COK0uU6ZaJI+LA67H/rp8IbEkYwlZf3mAoBI5wtPh5G5cbEQGNhVpzINg2f/6+q/YipnNIKy6fJDg6kMUKUw4Q==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.1", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", + "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.1" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.80.0.tgz", + "integrity": "sha512-q2LyCVJGN4p7d92cOI7scWOoNwxJhZuFRwiimSUGJGI5zX7ubf1WUPznwOmYEn8WVo3Io+MyMinA7era6j5KPw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.80.0.tgz", + "integrity": "sha512-0S/k8LRtoblrbzy4ir9m4WuvU/XTkb1EwL/33/oJexCUHCXtsqaPJ3eKfr1GWtNqTa1zryv6sXs3Fpv7lKCsMQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.80.0.tgz", + "integrity": "sha512-yKzehXlRbDoXIQefdRQnvaI9BEogoWIp/7+y/m5enZDKW2IP9aAgq5tU72sThcwftDJvknnIpEHAABG3qviEng==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.80.0.tgz", + "integrity": "sha512-cXK6Gs4UDylN8oz40omi01QK0cSCBVj0efXC1WodpENTuDnrkUs28W8/eslEnAtlawaVtikC1Q92mpz9+o85Mg==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.80.0.tgz", + "integrity": "sha512-Iepod83h2WoMCaLC9pGb3QOT67Kn3RlUdbXpo3uvbDKfPU8EgytS4RVaPmDjhqDjj8AGaiz9mk/ppd2Q2WS+gw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.80.0.tgz", + "integrity": "sha512-n8pkXQxuo5zCWXX5cbSNZj1vuWS8IVNGWTmP1m31Iq1k0e8lPZ07PF08TRV79HHq3mEPP/Ko//BQuflHvY2o8w==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.80.0", + "@supabase/functions-js": "2.80.0", + "@supabase/postgrest-js": "2.80.0", + "@supabase/realtime-js": "2.80.0", + "@supabase/storage-js": "2.80.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "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.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "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", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "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", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord.js": { + "version": "14.24.2", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.2.tgz", + "integrity": "sha512-VMEDbmguRdX/EeMaTsf9Mb0IQA90WdYF2cn4QDfslQFXgQ6LFtmlPn0FSotnS0kcFbFp+JBSIxtnF+bnAHG/hQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.1", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "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", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "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", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "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", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "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/attached_assets/bot2/discord-bot/package.json b/attached_assets/bot2/discord-bot/package.json new file mode 100644 index 0000000..b65f0ac --- /dev/null +++ b/attached_assets/bot2/discord-bot/package.json @@ -0,0 +1,34 @@ +{ + "name": "aethex-discord-bot", + "version": "1.0.0", + "description": "AeThex Discord Bot - Account linking, role management, and realm selection", + "main": "bot.js", + "type": "commonjs", + "scripts": { + "start": "node bot.js", + "dev": "nodemon bot.js", + "register-commands": "node scripts/register-commands.js" + }, + "keywords": [ + "discord", + "bot", + "aethex", + "role-management", + "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", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/attached_assets/bot2/discord-bot/scripts/register-commands.js b/attached_assets/bot2/discord-bot/scripts/register-commands.js new file mode 100644 index 0000000..27ffb8d --- /dev/null +++ b/attached_assets/bot2/discord-bot/scripts/register-commands.js @@ -0,0 +1,110 @@ +const { REST, Routes } = require("discord.js"); +const fs = require("fs"); +const path = require("path"); +require("dotenv").config(); + +// Validate environment variables +const requiredEnvVars = ["DISCORD_BOT_TOKEN", "DISCORD_CLIENT_ID"]; + +const missingVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); +if (missingVars.length > 0) { + console.error( + "โŒ FATAL ERROR: Missing required environment variables:", + missingVars.join(", "), + ); + console.error("\nPlease set these before running command registration:"); + missingVars.forEach((envVar) => { + console.error(` - ${envVar}`); + }); + process.exit(1); +} + +// Load commands from commands directory +const commandsPath = path.join(__dirname, "../commands"); +const commandFiles = fs + .readdirSync(commandsPath) + .filter((file) => file.endsWith(".js")); + +const commands = []; + +for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + const command = require(filePath); + if ("data" in command && "execute" in command) { + commands.push(command.data.toJSON()); + console.log(`โœ… Loaded command: ${command.data.name}`); + } +} + +// Register commands with Discord API +async function registerCommands() { + try { + const rest = new REST({ version: "10" }).setToken( + process.env.DISCORD_BOT_TOKEN, + ); + + console.log(`\n๐Ÿ“ Registering ${commands.length} slash commands...`); + console.log( + "โš ๏ธ This will co-exist with Discord's auto-generated Entry Point command.\n", + ); + + try { + const data = await rest.put( + Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), + { body: commands }, + ); + console.log(`โœ… Successfully registered ${data.length} slash commands.`); + console.log("\n๐ŸŽ‰ Command registration complete!"); + console.log("โ„น๏ธ Your commands are now live in Discord."); + console.log( + "โ„น๏ธ The Entry Point command (for Activities) will be managed by Discord.\n", + ); + } catch (error) { + // Handle Entry Point command conflict + if (error.code === 50240) { + console.warn( + "โš ๏ธ Error 50240: Entry Point command detected (Discord Activity enabled).", + ); + console.warn("Registering commands individually...\n"); + + let successCount = 0; + for (const command of commands) { + try { + await rest.post( + Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), + { body: command }, + ); + successCount++; + } catch (postError) { + if (postError.code === 50045) { + console.warn( + ` โš ๏ธ ${command.name}: Already registered (skipping)`, + ); + } else { + console.error(` โŒ ${command.name}: ${postError.message}`); + } + } + } + + console.log( + `\nโœ… Registered ${successCount} slash commands (individual mode).`, + ); + console.log("๐ŸŽ‰ Command registration complete!"); + console.log( + "โ„น๏ธ The Entry Point command will be managed by Discord.\n", + ); + } else { + throw error; + } + } + } catch (error) { + console.error( + "โŒ Fatal error registering commands:", + error.message || error, + ); + process.exit(1); + } +} + +// Run registration +registerCommands(); diff --git a/attached_assets/bot2/discord-bot/utils/roleManager.js b/attached_assets/bot2/discord-bot/utils/roleManager.js new file mode 100644 index 0000000..27be439 --- /dev/null +++ b/attached_assets/bot2/discord-bot/utils/roleManager.js @@ -0,0 +1,137 @@ +const { EmbedBuilder } = require("discord.js"); + +/** + * Assign Discord role based on user's arm and type + * @param {Guild} guild - Discord guild + * @param {string} discordId - Discord user ID + * @param {string} arm - User's primary arm (labs, gameforge, corp, foundation, devlink) + * @param {object} supabase - Supabase client + * @returns {Promise} - Success status + */ +async function assignRoleByArm(guild, discordId, arm, supabase) { + try { + // Fetch guild member + const member = await guild.members.fetch(discordId); + if (!member) { + console.warn(`Member not found: ${discordId}`); + return false; + } + + // Get role mapping from Supabase + const { data: mapping, error: mapError } = await supabase + .from("discord_role_mappings") + .select("discord_role") + .eq("arm", arm) + .eq("server_id", guild.id) + .maybeSingle(); + + if (mapError) { + console.error("Error fetching role mapping:", mapError); + return false; + } + + if (!mapping) { + console.warn( + `No role mapping found for arm: ${arm} in server: ${guild.id}`, + ); + return false; + } + + // Find role by name or ID + let roleToAssign = guild.roles.cache.find( + (r) => r.id === mapping.discord_role || r.name === mapping.discord_role, + ); + + if (!roleToAssign) { + console.warn(`Role not found: ${mapping.discord_role}`); + return false; + } + + // Remove old arm roles + const armRoles = member.roles.cache.filter((role) => + ["Labs", "GameForge", "Corp", "Foundation", "Dev-Link"].some((arm) => + role.name.includes(arm), + ), + ); + + for (const [, role] of armRoles) { + try { + if (role.id !== roleToAssign.id) { + await member.roles.remove(role); + } + } catch (e) { + console.warn(`Could not remove role ${role.name}: ${e.message}`); + } + } + + // Assign new role + if (!member.roles.cache.has(roleToAssign.id)) { + await member.roles.add(roleToAssign); + console.log( + `โœ… Assigned role ${roleToAssign.name} to ${member.user.tag}`, + ); + return true; + } + + return true; + } catch (error) { + console.error("Error assigning role:", error); + return false; + } +} + +/** + * Get user's primary arm from Supabase + * @param {string} discordId - Discord user ID + * @param {object} supabase - Supabase client + * @returns {Promise} - Primary arm (labs, gameforge, corp, foundation, devlink) + */ +async function getUserArm(discordId, supabase) { + try { + const { data: link, error } = await supabase + .from("discord_links") + .select("primary_arm") + .eq("discord_id", discordId) + .maybeSingle(); + + if (error) { + console.error("Error fetching user arm:", error); + return null; + } + + return link?.primary_arm || null; + } catch (error) { + console.error("Error getting user arm:", error); + return null; + } +} + +/** + * Sync roles for a user across all guilds + * @param {Client} client - Discord client + * @param {string} discordId - Discord user ID + * @param {string} arm - Primary arm + * @param {object} supabase - Supabase client + */ +async function syncRolesAcrossGuilds(client, discordId, arm, supabase) { + try { + for (const [, guild] of client.guilds.cache) { + try { + const member = await guild.members.fetch(discordId); + if (member) { + await assignRoleByArm(guild, discordId, arm, supabase); + } + } catch (e) { + console.warn(`Could not sync roles in guild ${guild.id}: ${e.message}`); + } + } + } catch (error) { + console.error("Error syncing roles across guilds:", error); + } +} + +module.exports = { + assignRoleByArm, + getUserArm, + syncRolesAcrossGuilds, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/.env.example b/attached_assets/discord-bot-source/discord-bot/.env.example new file mode 100644 index 0000000..a4ddc91 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/.env.example @@ -0,0 +1,23 @@ +# Discord Bot Configuration +DISCORD_BOT_TOKEN=your_bot_token_here +DISCORD_CLIENT_ID=your_client_id_here +DISCORD_PUBLIC_KEY=your_public_key_here + +# Supabase Configuration +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_SERVICE_ROLE=your_service_role_key_here + +# 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 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 diff --git a/attached_assets/discord-bot-source/discord-bot/DEPLOYMENT_GUIDE.md b/attached_assets/discord-bot-source/discord-bot/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..c90a3aa --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/Dockerfile b/attached_assets/discord-bot-source/discord-bot/Dockerfile new file mode 100644 index 0000000..279b52f --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/bot.js b/attached_assets/discord-bot-source/discord-bot/bot.js new file mode 100644 index 0000000..401b132 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/bot.js @@ -0,0 +1,803 @@ +const { + Client, + GatewayIntentBits, + REST, + Routes, + Collection, + EmbedBuilder, +} = require("discord.js"); +const { createClient } = require("@supabase/supabase-js"); +const http = require("http"); +const fs = require("fs"); +const path = require("path"); +require("dotenv").config(); + +const { setupFeedListener, sendPostToDiscord, getFeedChannelId } = require("./listeners/feedSync"); + +// Validate environment variables +const requiredEnvVars = [ + "DISCORD_BOT_TOKEN", + "DISCORD_CLIENT_ID", + "SUPABASE_URL", + "SUPABASE_SERVICE_ROLE", +]; + +const missingVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); +if (missingVars.length > 0) { + console.error( + "โŒ FATAL ERROR: Missing required environment variables:", + missingVars.join(", "), + ); + console.error("\nPlease set these in your Discloud/hosting environment:"); + missingVars.forEach((envVar) => { + console.error(` - ${envVar}`); + }); + process.exit(1); +} + +// Validate token format +const token = process.env.DISCORD_BOT_TOKEN; +if (!token || token.length < 20) { + console.error("โŒ FATAL ERROR: DISCORD_BOT_TOKEN is empty or invalid"); + console.error(` Length: ${token ? token.length : 0}`); + process.exit(1); +} + +console.log("[Token] Bot token loaded (length: " + token.length + " chars)"); + +// Initialize Discord client with message intents for feed sync +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.DirectMessages, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], +}); + +// Initialize Supabase +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE, +); + +// Store slash commands +client.commands = new Collection(); + +// Load commands from commands directory +const commandsPath = path.join(__dirname, "commands"); +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}`); + } +} + +// Load event handlers from events directory +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 listener: ${event.name}`); + } + } +} + +// Slash command interaction handler +client.on("interactionCreate", async (interaction) => { + if (!interaction.isChatInputCommand()) return; + + const command = client.commands.get(interaction.commandName); + + if (!command) { + console.warn( + `โš ๏ธ No command matching ${interaction.commandName} was found.`, + ); + return; + } + + try { + await command.execute(interaction, supabase, client); + } catch (error) { + console.error(`โŒ Error executing ${interaction.commandName}:`, error); + + const errorEmbed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Command Error") + .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 }); + } else { + await interaction.reply({ embeds: [errorEmbed], ephemeral: true }); + } + } +}); + +// IMPORTANT: Commands are now registered via a separate script +// Run this ONCE during deployment: npm run register-commands +// This prevents Error 50240 (Entry Point conflict) when Activities are enabled +// The bot will simply load and listen for the already-registered commands + +// Define all commands for registration +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, + }, + ], + }, +]; + +// Function to register commands with Discord +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 { + // Try bulk update first + 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) { + // Handle Error 50240 (Entry Point conflict) + 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 }; + } +} + +// Start HTTP health check server +const healthPort = process.env.HEALTH_PORT || 8080; +const ADMIN_TOKEN = process.env.DISCORD_ADMIN_TOKEN || "aethex-bot-admin"; + +// Helper to check admin authentication +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()), + timestamp: new Date().toISOString(), + }), + ); + return; + } + + // GET /bot-status - Comprehensive bot status for management panel (requires auth) + 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, + }, + timestamp: new Date().toISOString(), + }), + ); + return; + } + + // GET /linked-users - Get all Discord-linked users (requires auth, sanitizes PII) + if (req.url === "/linked-users") { + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + 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; + } + + // GET /command-stats - Get command usage statistics (requires auth) + if (req.url === "/command-stats") { + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + return; + } + + (async () => { + try { + 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 })); + } catch (error) { + res.writeHead(500); + res.end(JSON.stringify({ success: false, error: error.message })); + } + })(); + return; + } + + // GET /feed-stats - Get feed bridge statistics (requires auth) + if (req.url === "/feed-stats") { + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + 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; + } + + // POST /send-to-discord - Send a post from AeThex to Discord channel + if (req.url === "/send-to-discord" && req.method === "POST") { + let body = ""; + req.on("data", (chunk) => { + body += chunk.toString(); + }); + req.on("end", async () => { + try { + // Simple auth check + 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); + + const result = await sendPostToDiscord(post, post.author); + res.writeHead(result.success ? 200 : 500); + res.end(JSON.stringify(result)); + } catch (error) { + console.error("[API] Error processing send-to-discord:", error); + res.writeHead(500); + res.end(JSON.stringify({ error: error.message })); + } + }); + return; + } + + // GET /bridge-status - Check if bridge is configured + 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; + } + // Show HTML form with button + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(` + + + + Register Discord Commands + + + +
+

๐Ÿค– Discord Commands Registration

+

Click the button below to register all Discord slash commands (/verify, /set-realm, /profile, /unlink, /verify-role)

+ + + +
โณ Registering... please wait...
+
+
+ + + + + `); + return; + } + + if (req.method === "POST") { + // Verify admin token + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + return; + } + + // Register commands + 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`, + ); + }); + +// Login with error handling +client.login(process.env.DISCORD_BOT_TOKEN).catch((error) => { + console.error("โŒ FATAL ERROR: Failed to login to Discord"); + console.error(` Error Code: ${error.code}`); + console.error(` Error Message: ${error.message}`); + + if (error.code === "TokenInvalid") { + console.error("\nโš ๏ธ DISCORD_BOT_TOKEN is invalid!"); + console.error(" Possible causes:"); + console.error(" 1. Token has been revoked by Discord"); + console.error(" 2. Token has expired"); + console.error(" 3. Token format is incorrect"); + console.error( + "\n Solution: Get a new bot token from Discord Developer Portal", + ); + console.error(" https://discord.com/developers/applications"); + } + + process.exit(1); +}); + +client.once("ready", () => { + console.log(`โœ… Bot logged in as ${client.user.tag}`); + console.log(`๐Ÿ“ก Listening in ${client.guilds.cache.size} server(s)`); + console.log("โ„น๏ธ Commands are registered via: npm run register-commands"); + + // Set bot status + client.user.setActivity("/verify to link your AeThex account", { + type: "LISTENING", + }); + + // Setup bidirectional feed bridge (AeThex โ†’ Discord) + setupFeedListener(client); +}); + +// Error handling +process.on("unhandledRejection", (error) => { + console.error("โŒ Unhandled Promise Rejection:", error); +}); + +process.on("uncaughtException", (error) => { + console.error("โŒ Uncaught Exception:", error); + process.exit(1); +}); + +module.exports = client; diff --git a/attached_assets/discord-bot-source/discord-bot/commands/help.js b/attached_assets/discord-bot-source/discord-bot/commands/help.js new file mode 100644 index 0000000..324b1dd --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/commands/help.js @@ -0,0 +1,55 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("help") + .setDescription("View all AeThex bot commands and features"), + + async execute(interaction) { + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("๐Ÿค– AeThex Bot Commands") + .setDescription("Here are all the commands you can use with the AeThex Discord bot.") + .addFields( + { + name: "๐Ÿ”— Account Linking", + value: [ + "`/verify` - Link your Discord account to AeThex", + "`/unlink` - Disconnect your Discord from AeThex", + "`/profile` - View your linked AeThex profile", + ].join("\n"), + }, + { + name: "โš”๏ธ Realm Management", + value: [ + "`/set-realm` - Choose your primary realm (Labs, GameForge, Corp, Foundation, Dev-Link)", + "`/verify-role` - Check your assigned Discord roles", + ].join("\n"), + }, + { + name: "๐Ÿ“Š Community", + value: [ + "`/stats` - View your AeThex statistics and activity", + "`/leaderboard` - See the top contributors", + "`/post` - Create a post in the AeThex community feed", + ].join("\n"), + }, + { + name: "โ„น๏ธ Information", + value: "`/help` - Show this help message", + }, + ) + .addFields({ + name: "๐Ÿ”— Quick Links", + value: [ + "[AeThex Platform](https://aethex.dev)", + "[Creator Directory](https://aethex.dev/creators)", + "[Community Feed](https://aethex.dev/community/feed)", + ].join(" | "), + }) + .setFooter({ text: "AeThex | Build. Create. Connect." }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + }, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/commands/leaderboard.js b/attached_assets/discord-bot-source/discord-bot/commands/leaderboard.js new file mode 100644 index 0000000..cbc5b01 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/commands/leaderboard.js @@ -0,0 +1,155 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("leaderboard") + .setDescription("View the top AeThex contributors") + .addStringOption((option) => + option + .setName("category") + .setDescription("Leaderboard category") + .setRequired(false) + .addChoices( + { name: "๐Ÿ”ฅ Most Active (Posts)", value: "posts" }, + { name: "โค๏ธ Most Liked", value: "likes" }, + { name: "๐ŸŽจ Top Creators", value: "creators" } + ) + ), + + async execute(interaction, supabase) { + await interaction.deferReply(); + + try { + const category = interaction.options.getString("category") || "posts"; + + let leaderboardData = []; + let title = ""; + let emoji = ""; + + if (category === "posts") { + title = "Most Active Posters"; + emoji = "๐Ÿ”ฅ"; + + const { data: posts } = await supabase + .from("community_posts") + .select("user_id") + .not("user_id", "is", null); + + const postCounts = {}; + posts?.forEach((post) => { + postCounts[post.user_id] = (postCounts[post.user_id] || 0) + 1; + }); + + const sortedUsers = Object.entries(postCounts) + .sort(([, a], [, b]) => b - a) + .slice(0, 10); + + for (const [userId, count] of sortedUsers) { + const { data: profile } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", userId) + .single(); + + if (profile) { + leaderboardData.push({ + name: profile.full_name || profile.username || "Anonymous", + value: `${count} posts`, + username: profile.username, + }); + } + } + } else if (category === "likes") { + title = "Most Liked Users"; + emoji = "โค๏ธ"; + + const { data: posts } = await supabase + .from("community_posts") + .select("user_id, likes_count") + .not("user_id", "is", null) + .order("likes_count", { ascending: false }); + + const likeCounts = {}; + posts?.forEach((post) => { + likeCounts[post.user_id] = + (likeCounts[post.user_id] || 0) + (post.likes_count || 0); + }); + + const sortedUsers = Object.entries(likeCounts) + .sort(([, a], [, b]) => b - a) + .slice(0, 10); + + for (const [userId, count] of sortedUsers) { + const { data: profile } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", userId) + .single(); + + if (profile) { + leaderboardData.push({ + name: profile.full_name || profile.username || "Anonymous", + value: `${count} likes received`, + username: profile.username, + }); + } + } + } else if (category === "creators") { + title = "Top Creators"; + emoji = "๐ŸŽจ"; + + const { data: creators } = await supabase + .from("aethex_creators") + .select("user_id, total_projects, verified, featured") + .order("total_projects", { ascending: false }) + .limit(10); + + for (const creator of creators || []) { + const { data: profile } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", creator.user_id) + .single(); + + if (profile) { + const badges = []; + if (creator.verified) badges.push("โœ…"); + if (creator.featured) badges.push("โญ"); + + leaderboardData.push({ + name: profile.full_name || profile.username || "Anonymous", + value: `${creator.total_projects || 0} projects ${badges.join(" ")}`, + username: profile.username, + }); + } + } + } + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle(`${emoji} ${title}`) + .setDescription( + leaderboardData.length > 0 + ? leaderboardData + .map( + (user, index) => + `**${index + 1}.** ${user.name} - ${user.value}` + ) + .join("\n") + : "No data available yet. Be the first to contribute!" + ) + .setFooter({ text: "AeThex Leaderboard | Updated in real-time" }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Leaderboard command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to fetch leaderboard. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/commands/post.js b/attached_assets/discord-bot-source/discord-bot/commands/post.js new file mode 100644 index 0000000..61057e6 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/commands/post.js @@ -0,0 +1,144 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + ActionRowBuilder, +} = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("post") + .setDescription("Create a post in the AeThex community feed") + .addStringOption((option) => + option + .setName("content") + .setDescription("Your post content") + .setRequired(true) + .setMaxLength(500) + ) + .addStringOption((option) => + option + .setName("category") + .setDescription("Post category") + .setRequired(false) + .addChoices( + { name: "๐Ÿ’ฌ General", value: "general" }, + { name: "๐Ÿš€ Project Update", value: "project_update" }, + { name: "โ“ Question", value: "question" }, + { name: "๐Ÿ’ก Idea", value: "idea" }, + { name: "๐ŸŽ‰ Announcement", value: "announcement" } + ) + ) + .addAttachmentOption((option) => + option + .setName("image") + .setDescription("Attach an image to your post") + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started." + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", link.user_id) + .single(); + + const content = interaction.options.getString("content"); + const category = interaction.options.getString("category") || "general"; + const attachment = interaction.options.getAttachment("image"); + + let imageUrl = null; + if (attachment && attachment.contentType?.startsWith("image/")) { + imageUrl = attachment.url; + } + + const categoryLabels = { + general: "General", + project_update: "Project Update", + question: "Question", + idea: "Idea", + announcement: "Announcement", + }; + + const { data: post, error } = await supabase + .from("community_posts") + .insert({ + user_id: link.user_id, + content: content, + category: category, + arm_affiliation: link.primary_arm || "general", + image_url: imageUrl, + source: "discord", + discord_message_id: interaction.id, + discord_author_id: interaction.user.id, + discord_author_name: interaction.user.username, + discord_author_avatar: interaction.user.displayAvatarURL(), + }) + .select() + .single(); + + if (error) throw error; + + const successEmbed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Post Created!") + .setDescription(content.length > 100 ? content.slice(0, 100) + "..." : content) + .addFields( + { + name: "๐Ÿ“ Category", + value: categoryLabels[category], + inline: true, + }, + { + name: "โš”๏ธ Realm", + value: link.primary_arm || "general", + inline: true, + } + ); + + if (imageUrl) { + successEmbed.setImage(imageUrl); + } + + successEmbed + .addFields({ + name: "๐Ÿ”— View Post", + value: `[Open in AeThex](https://aethex.dev/community/feed)`, + }) + .setFooter({ text: "Your post is now live on AeThex!" }) + .setTimestamp(); + + await interaction.editReply({ embeds: [successEmbed] }); + } catch (error) { + console.error("Post command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to create post. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/commands/profile.js b/attached_assets/discord-bot-source/discord-bot/commands/profile.js new file mode 100644 index 0000000..035f251 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/commands/profile.js @@ -0,0 +1,93 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("profile") + .setDescription("View your AeThex profile in Discord"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("*") + .eq("id", link.user_id) + .single(); + + if (!profile) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Profile Not Found") + .setDescription("Your AeThex profile could not be found."); + + return await interaction.editReply({ embeds: [embed] }); + } + + const armEmojis = { + labs: "๐Ÿงช", + gameforge: "๐ŸŽฎ", + corp: "๐Ÿ’ผ", + foundation: "๐Ÿค", + devlink: "๐Ÿ’ป", + }; + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle(`${profile.full_name || "AeThex User"}'s Profile`) + .setThumbnail( + profile.avatar_url || "https://aethex.dev/placeholder.svg", + ) + .addFields( + { + name: "๐Ÿ‘ค Username", + value: profile.username || "N/A", + inline: true, + }, + { + name: `${armEmojis[link.primary_arm] || "โš”๏ธ"} Primary Realm`, + value: link.primary_arm || "Not set", + inline: true, + }, + { + name: "๐Ÿ“Š Role", + value: profile.user_type || "community_member", + inline: true, + }, + { name: "๐Ÿ“ Bio", value: profile.bio || "No bio set", inline: false }, + ) + .addFields({ + name: "๐Ÿ”— Links", + value: `[Visit Full Profile](https://aethex.dev/creators/${profile.username})`, + }) + .setFooter({ text: "AeThex | Your Web3 Creator Hub" }); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Profile command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to fetch profile. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/commands/refresh-roles.js b/attached_assets/discord-bot-source/discord-bot/commands/refresh-roles.js new file mode 100644 index 0000000..459bd79 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/commands/refresh-roles.js @@ -0,0 +1,72 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const { assignRoleByArm, getUserArm } = require("../utils/roleManager"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("refresh-roles") + .setDescription( + "Refresh your Discord roles based on your current AeThex settings", + ), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + // Check if user is linked + const { data: link } = await supabase + .from("discord_links") + .select("primary_arm") + .eq("discord_id", interaction.user.id) + .maybeSingle(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + if (!link.primary_arm) { + const embed = new EmbedBuilder() + .setColor(0xffaa00) + .setTitle("โš ๏ธ No Realm Set") + .setDescription( + "You haven't set your primary realm yet.\nUse `/set-realm` to choose one.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + // Assign role based on current primary arm + const roleAssigned = await assignRoleByArm( + interaction.guild, + interaction.user.id, + link.primary_arm, + supabase, + ); + + const embed = new EmbedBuilder() + .setColor(roleAssigned ? 0x00ff00 : 0xffaa00) + .setTitle("โœ… Roles Refreshed") + .setDescription( + roleAssigned + ? `Your Discord roles have been synced with your AeThex account.\n\nPrimary Realm: **${link.primary_arm}**` + : `Your roles could not be automatically assigned.\n\nPrimary Realm: **${link.primary_arm}**\n\nโš ๏ธ Please contact an admin to set up the role mapping for this server.`, + ); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Refresh-roles command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to refresh roles. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/commands/set-realm.js b/attached_assets/discord-bot-source/discord-bot/commands/set-realm.js new file mode 100644 index 0000000..c1af120 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/commands/set-realm.js @@ -0,0 +1,139 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + StringSelectMenuBuilder, + ActionRowBuilder, +} = require("discord.js"); +const { assignRoleByArm } = require("../utils/roleManager"); + +const REALMS = [ + { value: "labs", label: "๐Ÿงช Labs", description: "Research & Development" }, + { + value: "gameforge", + label: "๐ŸŽฎ GameForge", + description: "Game Development", + }, + { value: "corp", label: "๐Ÿ’ผ Corp", description: "Enterprise Solutions" }, + { + value: "foundation", + label: "๐Ÿค Foundation", + description: "Community & Education", + }, + { + value: "devlink", + label: "๐Ÿ’ป Dev-Link", + description: "Professional Networking", + }, +]; + +module.exports = { + data: new SlashCommandBuilder() + .setName("set-realm") + .setDescription("Set your primary AeThex realm/arm"), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const select = new StringSelectMenuBuilder() + .setCustomId("select_realm") + .setPlaceholder("Choose your primary realm") + .addOptions( + REALMS.map((realm) => ({ + label: realm.label, + description: realm.description, + value: realm.value, + default: realm.value === link.primary_arm, + })), + ); + + const row = new ActionRowBuilder().addComponents(select); + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("โš”๏ธ Choose Your Realm") + .setDescription( + "Select your primary AeThex realm. This determines your main Discord role.", + ) + .addFields({ + name: "Current Realm", + value: link.primary_arm || "Not set", + }); + + await interaction.editReply({ embeds: [embed], components: [row] }); + + const filter = (i) => + i.user.id === interaction.user.id && i.customId === "select_realm"; + const collector = interaction.channel.createMessageComponentCollector({ + filter, + time: 60000, + }); + + collector.on("collect", async (i) => { + const selectedRealm = i.values[0]; + + await supabase + .from("discord_links") + .update({ primary_arm: selectedRealm }) + .eq("discord_id", interaction.user.id); + + const realm = REALMS.find((r) => r.value === selectedRealm); + + // Assign Discord role based on selected realm + const roleAssigned = await assignRoleByArm( + interaction.guild, + interaction.user.id, + selectedRealm, + supabase, + ); + + const roleStatus = roleAssigned + ? "โœ… Discord role assigned!" + : "โš ๏ธ No role mapping found for this realm in this server."; + + const confirmEmbed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Realm Set") + .setDescription( + `Your primary realm is now **${realm.label}**\n\n${roleStatus}`, + ); + + await i.update({ embeds: [confirmEmbed], components: [] }); + }); + + collector.on("end", (collected) => { + if (collected.size === 0) { + interaction.editReply({ + content: "Realm selection timed out.", + components: [], + }); + } + }); + } catch (error) { + console.error("Set-realm command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to update realm. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/commands/stats.js b/attached_assets/discord-bot-source/discord-bot/commands/stats.js new file mode 100644 index 0000000..fe9814b --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/commands/stats.js @@ -0,0 +1,140 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("stats") + .setDescription("View your AeThex statistics and activity"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm, created_at") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started." + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("*") + .eq("id", link.user_id) + .single(); + + const { count: postCount } = await supabase + .from("community_posts") + .select("*", { count: "exact", head: true }) + .eq("user_id", link.user_id); + + const { count: likeCount } = await supabase + .from("community_likes") + .select("*", { count: "exact", head: true }) + .eq("user_id", link.user_id); + + const { count: commentCount } = await supabase + .from("community_comments") + .select("*", { count: "exact", head: true }) + .eq("user_id", link.user_id); + + const { data: creatorProfile } = await supabase + .from("aethex_creators") + .select("verified, featured, total_projects") + .eq("user_id", link.user_id) + .single(); + + const armEmojis = { + labs: "๐Ÿงช", + gameforge: "๐ŸŽฎ", + corp: "๐Ÿ’ผ", + foundation: "๐Ÿค", + devlink: "๐Ÿ’ป", + }; + + const linkedDate = new Date(link.created_at); + const daysSinceLinked = Math.floor( + (Date.now() - linkedDate.getTime()) / (1000 * 60 * 60 * 24) + ); + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle(`๐Ÿ“Š ${profile?.full_name || interaction.user.username}'s Stats`) + .setThumbnail(profile?.avatar_url || interaction.user.displayAvatarURL()) + .addFields( + { + name: `${armEmojis[link.primary_arm] || "โš”๏ธ"} Primary Realm`, + value: link.primary_arm || "Not set", + inline: true, + }, + { + name: "๐Ÿ‘ค Account Type", + value: profile?.user_type || "community_member", + inline: true, + }, + { + name: "๐Ÿ“… Days Linked", + value: `${daysSinceLinked} days`, + inline: true, + } + ) + .addFields( + { + name: "๐Ÿ“ Posts", + value: `${postCount || 0}`, + inline: true, + }, + { + name: "โค๏ธ Likes Given", + value: `${likeCount || 0}`, + inline: true, + }, + { + name: "๐Ÿ’ฌ Comments", + value: `${commentCount || 0}`, + inline: true, + } + ); + + if (creatorProfile) { + embed.addFields({ + name: "๐ŸŽจ Creator Status", + value: [ + creatorProfile.verified ? "โœ… Verified Creator" : "โณ Pending Verification", + creatorProfile.featured ? "โญ Featured" : "", + `๐Ÿ“ ${creatorProfile.total_projects || 0} Projects`, + ] + .filter(Boolean) + .join("\n"), + }); + } + + embed + .addFields({ + name: "๐Ÿ”— Full Profile", + value: `[View on AeThex](https://aethex.dev/creators/${profile?.username || link.user_id})`, + }) + .setFooter({ text: "AeThex | Your Creative Hub" }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Stats command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to fetch stats. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/commands/unlink.js b/attached_assets/discord-bot-source/discord-bot/commands/unlink.js new file mode 100644 index 0000000..ac06d2a --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/commands/unlink.js @@ -0,0 +1,75 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("unlink") + .setDescription("Unlink your Discord account from AeThex"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("*") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โ„น๏ธ Not Linked") + .setDescription("Your Discord account is not linked to AeThex."); + + return await interaction.editReply({ embeds: [embed] }); + } + + // Delete the link + await supabase + .from("discord_links") + .delete() + .eq("discord_id", interaction.user.id); + + // Remove Discord roles from user + const guild = interaction.guild; + const member = await guild.members.fetch(interaction.user.id); + + // Find and remove all AeThex-related roles + const rolesToRemove = member.roles.cache.filter( + (role) => + role.name.includes("Labs") || + role.name.includes("GameForge") || + role.name.includes("Corp") || + role.name.includes("Foundation") || + role.name.includes("Dev-Link") || + role.name.includes("Premium") || + role.name.includes("Creator"), + ); + + for (const [, role] of rolesToRemove) { + try { + await member.roles.remove(role); + } catch (e) { + console.warn(`Could not remove role ${role.name}`); + } + } + + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Account Unlinked") + .setDescription( + "Your Discord account has been unlinked from AeThex.\nAll associated roles have been removed.", + ); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Unlink command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to unlink account. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/commands/verify-role.js b/attached_assets/discord-bot-source/discord-bot/commands/verify-role.js new file mode 100644 index 0000000..1b7e6b9 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/commands/verify-role.js @@ -0,0 +1,97 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("verify-role") + .setDescription("Check your AeThex-assigned Discord roles"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("user_type") + .eq("id", link.user_id) + .single(); + + const { data: mappings } = await supabase + .from("discord_role_mappings") + .select("discord_role") + .eq("arm", link.primary_arm) + .eq("user_type", profile?.user_type || "community_member"); + + const member = await interaction.guild.members.fetch(interaction.user.id); + const aethexRoles = member.roles.cache.filter( + (role) => + role.name.includes("Labs") || + role.name.includes("GameForge") || + role.name.includes("Corp") || + role.name.includes("Foundation") || + role.name.includes("Dev-Link") || + role.name.includes("Premium") || + role.name.includes("Creator"), + ); + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("๐Ÿ” Your AeThex Roles") + .addFields( + { + name: "โš”๏ธ Primary Realm", + value: link.primary_arm || "Not set", + inline: true, + }, + { + name: "๐Ÿ‘ค User Type", + value: profile?.user_type || "community_member", + inline: true, + }, + { + name: "๐ŸŽญ Discord Roles", + value: + aethexRoles.size > 0 + ? aethexRoles.map((r) => r.name).join(", ") + : "None assigned yet", + }, + { + name: "๐Ÿ“‹ Expected Roles", + value: + mappings?.length > 0 + ? mappings.map((m) => m.discord_role).join(", ") + : "No mappings found", + }, + ) + .setFooter({ + text: "Roles are assigned automatically based on your AeThex profile", + }); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Verify-role command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to verify roles. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/commands/verify.js b/attached_assets/discord-bot-source/discord-bot/commands/verify.js new file mode 100644 index 0000000..d9f30e7 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/commands/verify.js @@ -0,0 +1,85 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, +} = require("discord.js"); +const { syncRolesAcrossGuilds } = require("../utils/roleManager"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("verify") + .setDescription("Link your Discord account to your AeThex account"), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: existingLink } = await supabase + .from("discord_links") + .select("*") + .eq("discord_id", interaction.user.id) + .single(); + + if (existingLink) { + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Already Linked") + .setDescription( + `Your Discord account is already linked to AeThex (User ID: ${existingLink.user_id})`, + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + // Generate verification code + const verificationCode = Math.random() + .toString(36) + .substring(2, 8) + .toUpperCase(); + const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes + + // Store verification code with Discord username + await supabase.from("discord_verifications").insert({ + discord_id: interaction.user.id, + verification_code: verificationCode, + username: interaction.user.username, + expires_at: expiresAt.toISOString(), + }); + + const verifyUrl = `https://aethex.dev/discord-verify?code=${verificationCode}`; + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("๐Ÿ”— Link Your AeThex Account") + .setDescription( + "Click the button below to link your Discord account to AeThex.", + ) + .addFields( + { name: "โฑ๏ธ Expires In", value: "15 minutes" }, + { name: "๐Ÿ“ Verification Code", value: `\`${verificationCode}\`` }, + ) + .setFooter({ text: "Your security code will expire in 15 minutes" }); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("Link Account") + .setStyle(ButtonStyle.Link) + .setURL(verifyUrl), + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); + } catch (error) { + console.error("Verify command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription( + "Failed to generate verification code. Please try again.", + ); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/discloud.config b/attached_assets/discord-bot-source/discord-bot/discloud.config new file mode 100644 index 0000000..fe114e6 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/events/messageCreate.js b/attached_assets/discord-bot-source/discord-bot/events/messageCreate.js new file mode 100644 index 0000000..75abc33 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/events/messageCreate.js @@ -0,0 +1,180 @@ +const { createClient } = require("@supabase/supabase-js"); + +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE, +); + +// Only sync messages from this specific channel +const FEED_CHANNEL_ID = process.env.DISCORD_MAIN_CHAT_CHANNELS + ? process.env.DISCORD_MAIN_CHAT_CHANNELS.split(",")[0].trim() + : null; + +function getArmAffiliation(message) { + const guildName = message.guild?.name?.toLowerCase() || ""; + const channelName = message.channel?.name?.toLowerCase() || ""; + const searchString = `${guildName} ${channelName}`; + + if (searchString.includes("gameforge")) return "gameforge"; + if (searchString.includes("corp")) return "corp"; + if (searchString.includes("foundation")) return "foundation"; + if (searchString.includes("devlink") || searchString.includes("dev-link")) + return "devlink"; + if (searchString.includes("nexus")) return "nexus"; + if (searchString.includes("staff")) return "staff"; + + return "labs"; +} + +async function syncMessageToFeed(message) { + try { + console.log( + `[Feed Sync] Processing from ${message.author.tag} in #${message.channel.name}`, + ); + + const { data: linkedAccount } = await supabase + .from("discord_links") + .select("user_id") + .eq("discord_id", message.author.id) + .single(); + + let authorId = linkedAccount?.user_id; + let authorInfo = null; + + if (authorId) { + const { data: profile } = await supabase + .from("user_profiles") + .select("id, username, full_name, avatar_url") + .eq("id", authorId) + .single(); + authorInfo = profile; + } + + if (!authorId) { + const discordUsername = `discord-${message.author.id}`; + let { data: guestProfile } = await supabase + .from("user_profiles") + .select("id, username, full_name, avatar_url") + .eq("username", discordUsername) + .single(); + + if (!guestProfile) { + const { data: newProfile, error: createError } = await supabase + .from("user_profiles") + .insert({ + username: discordUsername, + full_name: message.author.displayName || message.author.username, + avatar_url: message.author.displayAvatarURL({ size: 256 }), + }) + .select("id, username, full_name, avatar_url") + .single(); + + if (createError) { + console.error("[Feed Sync] Could not create guest profile:", createError); + return; + } + guestProfile = newProfile; + } + + authorId = guestProfile?.id; + authorInfo = guestProfile; + } + + if (!authorId) { + console.error("[Feed Sync] Could not get author ID"); + return; + } + + let content = message.content || "Shared a message on Discord"; + let mediaUrl = null; + let mediaType = "none"; + + if (message.attachments.size > 0) { + const attachment = message.attachments.first(); + if (attachment) { + mediaUrl = attachment.url; + const attachmentLower = attachment.name.toLowerCase(); + + if ( + [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) => + attachmentLower.endsWith(ext), + ) + ) { + mediaType = "image"; + } else if ( + [".mp4", ".webm", ".mov", ".avi"].some((ext) => + attachmentLower.endsWith(ext), + ) + ) { + mediaType = "video"; + } + } + } + + const armAffiliation = getArmAffiliation(message); + + const postContent = JSON.stringify({ + text: content, + mediaUrl: mediaUrl, + mediaType: mediaType, + source: "discord", + discord_message_id: message.id, + discord_channel_id: message.channelId, + discord_channel_name: message.channel.name, + discord_guild_id: message.guildId, + discord_guild_name: message.guild?.name, + discord_author_id: message.author.id, + discord_author_tag: message.author.tag, + discord_author_avatar: message.author.displayAvatarURL({ size: 256 }), + is_linked_user: !!linkedAccount, + }); + + const { error: insertError } = await supabase + .from("community_posts") + .insert({ + title: content.substring(0, 100) || "Discord Message", + content: postContent, + arm_affiliation: armAffiliation, + author_id: authorId, + tags: ["discord", "feed"], + category: "discord", + is_published: true, + likes_count: 0, + comments_count: 0, + }); + + if (insertError) { + console.error("[Feed Sync] Post creation failed:", insertError); + return; + } + + console.log( + `[Feed Sync] โœ… Synced message from ${message.author.tag} to AeThex feed`, + ); + } catch (error) { + console.error("[Feed Sync] Error:", error); + } +} + +module.exports = { + name: "messageCreate", + async execute(message, client) { + // Ignore bot messages + if (message.author.bot) return; + + // Ignore empty messages + if (!message.content && message.attachments.size === 0) return; + + // Only process messages from the configured feed channel + if (!FEED_CHANNEL_ID) { + return; // No channel configured + } + + if (message.channelId !== FEED_CHANNEL_ID) { + return; // Not the feed channel + } + + // Sync this message to AeThex feed + await syncMessageToFeed(message); + }, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/listeners/feedSync.js b/attached_assets/discord-bot-source/discord-bot/listeners/feedSync.js new file mode 100644 index 0000000..b8168c4 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/listeners/feedSync.js @@ -0,0 +1,239 @@ +const { EmbedBuilder } = require("discord.js"); +const { createClient } = require("@supabase/supabase-js"); + +const 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() + : null; + +const POLL_INTERVAL = 5000; // Check every 5 seconds + +let discordClient = null; +let lastCheckedTime = null; +let pollInterval = null; +let isPolling = false; // Concurrency lock to prevent overlapping polls +const processedPostIds = new Set(); // Track already-processed posts to prevent duplicates + +function getArmColor(arm) { + const colors = { + labs: 0x00d4ff, + gameforge: 0xff6b00, + corp: 0x9945ff, + foundation: 0x14f195, + devlink: 0xf7931a, + nexus: 0xff00ff, + staff: 0xffd700, + }; + return colors[arm] || 0x5865f2; +} + +function getArmEmoji(arm) { + const emojis = { + labs: "๐Ÿ”ฌ", + gameforge: "๐ŸŽฎ", + corp: "๐Ÿข", + foundation: "๐ŸŽ“", + devlink: "๐Ÿ”—", + nexus: "๐ŸŒ", + staff: "โญ", + }; + return emojis[arm] || "๐Ÿ“"; +} + +async function sendPostToDiscord(post, authorInfo = null) { + if (!discordClient || !FEED_CHANNEL_ID) { + console.log("[Feed Bridge] No Discord client or channel configured"); + return { success: false, error: "No Discord client or channel configured" }; + } + + try { + const channel = await discordClient.channels.fetch(FEED_CHANNEL_ID); + if (!channel || !channel.isTextBased()) { + console.error("[Feed Bridge] Could not find text channel:", FEED_CHANNEL_ID); + return { success: false, error: "Could not find text channel" }; + } + + let content = {}; + try { + content = typeof post.content === "string" ? JSON.parse(post.content) : post.content; + } catch { + content = { text: post.content }; + } + + if (content.source === "discord") { + return { success: true, skipped: true, reason: "Discord-sourced post" }; + } + + let author = authorInfo; + if (!author && post.author_id) { + const { data } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", post.author_id) + .single(); + author = data; + } + + const authorName = author?.full_name || author?.username || "AeThex User"; + // Discord only accepts HTTP/HTTPS URLs for icons - filter out base64/data URLs + const rawAvatar = author?.avatar_url || ""; + const authorAvatar = rawAvatar.startsWith("http://") || rawAvatar.startsWith("https://") + ? rawAvatar + : "https://aethex.dev/logo.png"; + const arm = post.arm_affiliation || "labs"; + + const embed = new EmbedBuilder() + .setColor(getArmColor(arm)) + .setAuthor({ + name: `${getArmEmoji(arm)} ${authorName}`, + iconURL: authorAvatar, + url: `https://aethex.dev/creators/${author?.username || post.author_id}`, + }) + .setDescription(content.text || post.title || "New post") + .setTimestamp(post.created_at ? new Date(post.created_at) : new Date()) + .setFooter({ + text: `Posted from AeThex โ€ข ${arm.charAt(0).toUpperCase() + arm.slice(1)}`, + iconURL: "https://aethex.dev/logo.png", + }); + + if (content.mediaUrl) { + if (content.mediaType === "image") { + embed.setImage(content.mediaUrl); + } else if (content.mediaType === "video") { + embed.addFields({ + name: "๐ŸŽฌ Video", + value: `[Watch Video](${content.mediaUrl})`, + }); + } + } + + if (post.tags && post.tags.length > 0) { + const tagString = post.tags + .filter((t) => t !== "discord" && t !== "main-chat") + .map((t) => `#${t}`) + .join(" "); + if (tagString) { + embed.addFields({ name: "Tags", value: tagString, inline: true }); + } + } + + const postUrl = `https://aethex.dev/community/feed?post=${post.id}`; + embed.addFields({ + name: "๐Ÿ”— View on AeThex", + value: `[Open Post](${postUrl})`, + inline: true, + }); + + await channel.send({ embeds: [embed] }); + console.log(`[Feed Bridge] โœ… Sent post ${post.id} to Discord`); + return { success: true }; + } catch (error) { + console.error("[Feed Bridge] Error sending to Discord:", error); + return { success: false, error: error.message }; + } +} + +async function checkForNewPosts() { + if (!discordClient || !FEED_CHANNEL_ID) return; + + // Prevent overlapping polls - if already polling, skip this run + if (isPolling) { + console.log("[Feed Bridge] Skipping poll - previous poll still in progress"); + return; + } + + isPolling = true; + + try { + const { data: posts, error } = await supabase + .from("community_posts") + .select("*") + .gt("created_at", lastCheckedTime.toISOString()) + .order("created_at", { ascending: true }); + + if (error) { + console.error("[Feed Bridge] Error fetching new posts:", error); + return; + } + + if (posts && posts.length > 0) { + // Update lastCheckedTime IMMEDIATELY after fetching to prevent re-fetching same posts + lastCheckedTime = new Date(posts[posts.length - 1].created_at); + + // Filter out already-processed posts (double safety) + const newPosts = posts.filter(post => !processedPostIds.has(post.id)); + + if (newPosts.length > 0) { + console.log(`[Feed Bridge] Found ${newPosts.length} new post(s)`); + + for (const post of newPosts) { + // Mark as processed BEFORE sending to prevent duplicates + processedPostIds.add(post.id); + + let content = {}; + try { + content = typeof post.content === "string" ? JSON.parse(post.content) : post.content; + } catch { + content = { text: post.content }; + } + + if (content.source === "discord") { + console.log(`[Feed Bridge] Skipping Discord-sourced post ${post.id}`); + continue; + } + + console.log(`[Feed Bridge] Bridging post ${post.id} to Discord...`); + await sendPostToDiscord(post); + } + } + + // Keep processedPostIds from growing indefinitely - trim old entries + if (processedPostIds.size > 1000) { + const idsArray = Array.from(processedPostIds); + idsArray.slice(0, 500).forEach(id => processedPostIds.delete(id)); + } + } + } catch (error) { + console.error("[Feed Bridge] Poll error:", error); + } finally { + isPolling = false; + } +} + +function setupFeedListener(client) { + discordClient = client; + + if (!FEED_CHANNEL_ID) { + console.log("[Feed Bridge] No DISCORD_MAIN_CHAT_CHANNELS configured - bridge disabled"); + return; + } + + lastCheckedTime = new Date(); + + console.log("[Feed Bridge] Starting polling for new posts (every 5 seconds)..."); + + pollInterval = setInterval(checkForNewPosts, POLL_INTERVAL); + + console.log("[Feed Bridge] โœ… Feed bridge ready (channel: " + FEED_CHANNEL_ID + ")"); +} + +function getDiscordClient() { + return discordClient; +} + +function getFeedChannelId() { + return FEED_CHANNEL_ID; +} + +function cleanup() { + if (pollInterval) { + clearInterval(pollInterval); + console.log("[Feed Bridge] Stopped polling"); + } +} + +module.exports = { setupFeedListener, sendPostToDiscord, getDiscordClient, getFeedChannelId, cleanup }; diff --git a/attached_assets/discord-bot-source/discord-bot/package-lock.json b/attached_assets/discord-bot-source/discord-bot/package-lock.json new file mode 100644 index 0000000..15b33b8 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/package-lock.json @@ -0,0 +1,1157 @@ +{ + "name": "aethex-discord-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aethex-discord-bot", + "version": "1.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", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "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.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.0.tgz", + "integrity": "sha512-COK0uU6ZaJI+LA67H/rp8IbEkYwlZf3mAoBI5wtPh5G5cbEQGNhVpzINg2f/6+q/YipnNIKy6fJDg6kMUKUw4Q==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.1", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", + "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.1" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.80.0.tgz", + "integrity": "sha512-q2LyCVJGN4p7d92cOI7scWOoNwxJhZuFRwiimSUGJGI5zX7ubf1WUPznwOmYEn8WVo3Io+MyMinA7era6j5KPw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.80.0.tgz", + "integrity": "sha512-0S/k8LRtoblrbzy4ir9m4WuvU/XTkb1EwL/33/oJexCUHCXtsqaPJ3eKfr1GWtNqTa1zryv6sXs3Fpv7lKCsMQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.80.0.tgz", + "integrity": "sha512-yKzehXlRbDoXIQefdRQnvaI9BEogoWIp/7+y/m5enZDKW2IP9aAgq5tU72sThcwftDJvknnIpEHAABG3qviEng==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.80.0.tgz", + "integrity": "sha512-cXK6Gs4UDylN8oz40omi01QK0cSCBVj0efXC1WodpENTuDnrkUs28W8/eslEnAtlawaVtikC1Q92mpz9+o85Mg==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.80.0.tgz", + "integrity": "sha512-Iepod83h2WoMCaLC9pGb3QOT67Kn3RlUdbXpo3uvbDKfPU8EgytS4RVaPmDjhqDjj8AGaiz9mk/ppd2Q2WS+gw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.80.0.tgz", + "integrity": "sha512-n8pkXQxuo5zCWXX5cbSNZj1vuWS8IVNGWTmP1m31Iq1k0e8lPZ07PF08TRV79HHq3mEPP/Ko//BQuflHvY2o8w==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.80.0", + "@supabase/functions-js": "2.80.0", + "@supabase/postgrest-js": "2.80.0", + "@supabase/realtime-js": "2.80.0", + "@supabase/storage-js": "2.80.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "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.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "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", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "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", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord.js": { + "version": "14.24.2", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.2.tgz", + "integrity": "sha512-VMEDbmguRdX/EeMaTsf9Mb0IQA90WdYF2cn4QDfslQFXgQ6LFtmlPn0FSotnS0kcFbFp+JBSIxtnF+bnAHG/hQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.1", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "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", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "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", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "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", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "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/attached_assets/discord-bot-source/discord-bot/package.json b/attached_assets/discord-bot-source/discord-bot/package.json new file mode 100644 index 0000000..b65f0ac --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/package.json @@ -0,0 +1,34 @@ +{ + "name": "aethex-discord-bot", + "version": "1.0.0", + "description": "AeThex Discord Bot - Account linking, role management, and realm selection", + "main": "bot.js", + "type": "commonjs", + "scripts": { + "start": "node bot.js", + "dev": "nodemon bot.js", + "register-commands": "node scripts/register-commands.js" + }, + "keywords": [ + "discord", + "bot", + "aethex", + "role-management", + "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", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/attached_assets/discord-bot-source/discord-bot/scripts/register-commands.js b/attached_assets/discord-bot-source/discord-bot/scripts/register-commands.js new file mode 100644 index 0000000..27ffb8d --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/scripts/register-commands.js @@ -0,0 +1,110 @@ +const { REST, Routes } = require("discord.js"); +const fs = require("fs"); +const path = require("path"); +require("dotenv").config(); + +// Validate environment variables +const requiredEnvVars = ["DISCORD_BOT_TOKEN", "DISCORD_CLIENT_ID"]; + +const missingVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); +if (missingVars.length > 0) { + console.error( + "โŒ FATAL ERROR: Missing required environment variables:", + missingVars.join(", "), + ); + console.error("\nPlease set these before running command registration:"); + missingVars.forEach((envVar) => { + console.error(` - ${envVar}`); + }); + process.exit(1); +} + +// Load commands from commands directory +const commandsPath = path.join(__dirname, "../commands"); +const commandFiles = fs + .readdirSync(commandsPath) + .filter((file) => file.endsWith(".js")); + +const commands = []; + +for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + const command = require(filePath); + if ("data" in command && "execute" in command) { + commands.push(command.data.toJSON()); + console.log(`โœ… Loaded command: ${command.data.name}`); + } +} + +// Register commands with Discord API +async function registerCommands() { + try { + const rest = new REST({ version: "10" }).setToken( + process.env.DISCORD_BOT_TOKEN, + ); + + console.log(`\n๐Ÿ“ Registering ${commands.length} slash commands...`); + console.log( + "โš ๏ธ This will co-exist with Discord's auto-generated Entry Point command.\n", + ); + + try { + const data = await rest.put( + Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), + { body: commands }, + ); + console.log(`โœ… Successfully registered ${data.length} slash commands.`); + console.log("\n๐ŸŽ‰ Command registration complete!"); + console.log("โ„น๏ธ Your commands are now live in Discord."); + console.log( + "โ„น๏ธ The Entry Point command (for Activities) will be managed by Discord.\n", + ); + } catch (error) { + // Handle Entry Point command conflict + if (error.code === 50240) { + console.warn( + "โš ๏ธ Error 50240: Entry Point command detected (Discord Activity enabled).", + ); + console.warn("Registering commands individually...\n"); + + let successCount = 0; + for (const command of commands) { + try { + await rest.post( + Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), + { body: command }, + ); + successCount++; + } catch (postError) { + if (postError.code === 50045) { + console.warn( + ` โš ๏ธ ${command.name}: Already registered (skipping)`, + ); + } else { + console.error(` โŒ ${command.name}: ${postError.message}`); + } + } + } + + console.log( + `\nโœ… Registered ${successCount} slash commands (individual mode).`, + ); + console.log("๐ŸŽ‰ Command registration complete!"); + console.log( + "โ„น๏ธ The Entry Point command will be managed by Discord.\n", + ); + } else { + throw error; + } + } + } catch (error) { + console.error( + "โŒ Fatal error registering commands:", + error.message || error, + ); + process.exit(1); + } +} + +// Run registration +registerCommands(); diff --git a/attached_assets/discord-bot-source/discord-bot/utils/roleManager.js b/attached_assets/discord-bot-source/discord-bot/utils/roleManager.js new file mode 100644 index 0000000..27be439 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/utils/roleManager.js @@ -0,0 +1,137 @@ +const { EmbedBuilder } = require("discord.js"); + +/** + * Assign Discord role based on user's arm and type + * @param {Guild} guild - Discord guild + * @param {string} discordId - Discord user ID + * @param {string} arm - User's primary arm (labs, gameforge, corp, foundation, devlink) + * @param {object} supabase - Supabase client + * @returns {Promise} - Success status + */ +async function assignRoleByArm(guild, discordId, arm, supabase) { + try { + // Fetch guild member + const member = await guild.members.fetch(discordId); + if (!member) { + console.warn(`Member not found: ${discordId}`); + return false; + } + + // Get role mapping from Supabase + const { data: mapping, error: mapError } = await supabase + .from("discord_role_mappings") + .select("discord_role") + .eq("arm", arm) + .eq("server_id", guild.id) + .maybeSingle(); + + if (mapError) { + console.error("Error fetching role mapping:", mapError); + return false; + } + + if (!mapping) { + console.warn( + `No role mapping found for arm: ${arm} in server: ${guild.id}`, + ); + return false; + } + + // Find role by name or ID + let roleToAssign = guild.roles.cache.find( + (r) => r.id === mapping.discord_role || r.name === mapping.discord_role, + ); + + if (!roleToAssign) { + console.warn(`Role not found: ${mapping.discord_role}`); + return false; + } + + // Remove old arm roles + const armRoles = member.roles.cache.filter((role) => + ["Labs", "GameForge", "Corp", "Foundation", "Dev-Link"].some((arm) => + role.name.includes(arm), + ), + ); + + for (const [, role] of armRoles) { + try { + if (role.id !== roleToAssign.id) { + await member.roles.remove(role); + } + } catch (e) { + console.warn(`Could not remove role ${role.name}: ${e.message}`); + } + } + + // Assign new role + if (!member.roles.cache.has(roleToAssign.id)) { + await member.roles.add(roleToAssign); + console.log( + `โœ… Assigned role ${roleToAssign.name} to ${member.user.tag}`, + ); + return true; + } + + return true; + } catch (error) { + console.error("Error assigning role:", error); + return false; + } +} + +/** + * Get user's primary arm from Supabase + * @param {string} discordId - Discord user ID + * @param {object} supabase - Supabase client + * @returns {Promise} - Primary arm (labs, gameforge, corp, foundation, devlink) + */ +async function getUserArm(discordId, supabase) { + try { + const { data: link, error } = await supabase + .from("discord_links") + .select("primary_arm") + .eq("discord_id", discordId) + .maybeSingle(); + + if (error) { + console.error("Error fetching user arm:", error); + return null; + } + + return link?.primary_arm || null; + } catch (error) { + console.error("Error getting user arm:", error); + return null; + } +} + +/** + * Sync roles for a user across all guilds + * @param {Client} client - Discord client + * @param {string} discordId - Discord user ID + * @param {string} arm - Primary arm + * @param {object} supabase - Supabase client + */ +async function syncRolesAcrossGuilds(client, discordId, arm, supabase) { + try { + for (const [, guild] of client.guilds.cache) { + try { + const member = await guild.members.fetch(discordId); + if (member) { + await assignRoleByArm(guild, discordId, arm, supabase); + } + } catch (e) { + console.warn(`Could not sync roles in guild ${guild.id}: ${e.message}`); + } + } + } catch (error) { + console.error("Error syncing roles across guilds:", error); + } +} + +module.exports = { + assignRoleByArm, + getUserArm, + syncRolesAcrossGuilds, +}; diff --git a/attached_assets/discord-bot_(1)_1765057157676.zip b/attached_assets/discord-bot_(1)_1765057157676.zip new file mode 100644 index 0000000..91d7d7e Binary files /dev/null and b/attached_assets/discord-bot_(1)_1765057157676.zip differ diff --git a/attached_assets/discord-bot_1765057157677.zip b/attached_assets/discord-bot_1765057157677.zip new file mode 100644 index 0000000..ce8e081 Binary files /dev/null and b/attached_assets/discord-bot_1765057157677.zip differ diff --git a/attached_assets/discord-bot_1765148924805.zip b/attached_assets/discord-bot_1765148924805.zip new file mode 100644 index 0000000..b89da89 Binary files /dev/null and b/attached_assets/discord-bot_1765148924805.zip differ