From 02e50ed47878660540543cd7c9bee54d55472da1 Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Sat, 6 Dec 2025 21:44:38 +0000 Subject: [PATCH] Add Discord bot functionality for AeThex platform integration Adds two Node.js Discord bots for the AeThex platform, enabling features such as account linking, role management, profile viewing, community posts, and more, with extensive Supabase integration. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e72fc1b7-94bd-4d6c-801f-cbac2fae245c Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: f8fda02a-6ff3-4bdf-87d4-fdbef7f9a2ce Replit-Helium-Checkpoint-Created: true --- .replit | 5 +- attached_assets/bot1/discord-bot/.env.example | 23 + .../bot1/discord-bot/DEPLOYMENT_GUIDE.md | 211 +++ attached_assets/bot1/discord-bot/Dockerfile | 22 + attached_assets/bot1/discord-bot/bot.js | 522 ++++++++ .../bot1/discord-bot/commands/profile.js | 93 ++ .../discord-bot/commands/refresh-roles.js | 72 + .../bot1/discord-bot/commands/set-realm.js | 139 ++ .../bot1/discord-bot/commands/unlink.js | 75 ++ .../bot1/discord-bot/commands/verify-role.js | 97 ++ .../bot1/discord-bot/commands/verify.js | 84 ++ .../bot1/discord-bot/discloud.config | 10 + .../events/messageCreate-announcements.js | 237 ++++ .../bot1/discord-bot/events/messageCreate.js | 320 +++++ .../bot1/discord-bot/package-lock.json | 1157 +++++++++++++++++ attached_assets/bot1/discord-bot/package.json | 34 + .../discord-bot/scripts/register-commands.js | 110 ++ .../bot1/discord-bot/utils/roleManager.js | 137 ++ attached_assets/bot2/discord-bot/.env.example | 23 + .../bot2/discord-bot/DEPLOYMENT_GUIDE.md | 211 +++ attached_assets/bot2/discord-bot/Dockerfile | 22 + attached_assets/bot2/discord-bot/bot.js | 803 ++++++++++++ .../bot2/discord-bot/commands/help.js | 55 + .../bot2/discord-bot/commands/leaderboard.js | 155 +++ .../bot2/discord-bot/commands/post.js | 144 ++ .../bot2/discord-bot/commands/profile.js | 93 ++ .../discord-bot/commands/refresh-roles.js | 72 + .../bot2/discord-bot/commands/set-realm.js | 139 ++ .../bot2/discord-bot/commands/stats.js | 140 ++ .../bot2/discord-bot/commands/unlink.js | 75 ++ .../bot2/discord-bot/commands/verify-role.js | 97 ++ .../bot2/discord-bot/commands/verify.js | 85 ++ .../bot2/discord-bot/discloud.config | 10 + .../bot2/discord-bot/events/messageCreate.js | 180 +++ .../bot2/discord-bot/listeners/feedSync.js | 239 ++++ .../bot2/discord-bot/package-lock.json | 1157 +++++++++++++++++ attached_assets/bot2/discord-bot/package.json | 34 + .../discord-bot/scripts/register-commands.js | 110 ++ .../bot2/discord-bot/utils/roleManager.js | 137 ++ .../discord-bot_(1)_1765057157676.zip | Bin 0 -> 44006 bytes attached_assets/discord-bot_1765057157677.zip | Bin 0 -> 37140 bytes 41 files changed, 7328 insertions(+), 1 deletion(-) create mode 100644 attached_assets/bot1/discord-bot/.env.example create mode 100644 attached_assets/bot1/discord-bot/DEPLOYMENT_GUIDE.md create mode 100644 attached_assets/bot1/discord-bot/Dockerfile create mode 100644 attached_assets/bot1/discord-bot/bot.js create mode 100644 attached_assets/bot1/discord-bot/commands/profile.js create mode 100644 attached_assets/bot1/discord-bot/commands/refresh-roles.js create mode 100644 attached_assets/bot1/discord-bot/commands/set-realm.js create mode 100644 attached_assets/bot1/discord-bot/commands/unlink.js create mode 100644 attached_assets/bot1/discord-bot/commands/verify-role.js create mode 100644 attached_assets/bot1/discord-bot/commands/verify.js create mode 100644 attached_assets/bot1/discord-bot/discloud.config create mode 100644 attached_assets/bot1/discord-bot/events/messageCreate-announcements.js create mode 100644 attached_assets/bot1/discord-bot/events/messageCreate.js create mode 100644 attached_assets/bot1/discord-bot/package-lock.json create mode 100644 attached_assets/bot1/discord-bot/package.json create mode 100644 attached_assets/bot1/discord-bot/scripts/register-commands.js create mode 100644 attached_assets/bot1/discord-bot/utils/roleManager.js create mode 100644 attached_assets/bot2/discord-bot/.env.example create mode 100644 attached_assets/bot2/discord-bot/DEPLOYMENT_GUIDE.md create mode 100644 attached_assets/bot2/discord-bot/Dockerfile create mode 100644 attached_assets/bot2/discord-bot/bot.js create mode 100644 attached_assets/bot2/discord-bot/commands/help.js create mode 100644 attached_assets/bot2/discord-bot/commands/leaderboard.js create mode 100644 attached_assets/bot2/discord-bot/commands/post.js create mode 100644 attached_assets/bot2/discord-bot/commands/profile.js create mode 100644 attached_assets/bot2/discord-bot/commands/refresh-roles.js create mode 100644 attached_assets/bot2/discord-bot/commands/set-realm.js create mode 100644 attached_assets/bot2/discord-bot/commands/stats.js create mode 100644 attached_assets/bot2/discord-bot/commands/unlink.js create mode 100644 attached_assets/bot2/discord-bot/commands/verify-role.js create mode 100644 attached_assets/bot2/discord-bot/commands/verify.js create mode 100644 attached_assets/bot2/discord-bot/discloud.config create mode 100644 attached_assets/bot2/discord-bot/events/messageCreate.js create mode 100644 attached_assets/bot2/discord-bot/listeners/feedSync.js create mode 100644 attached_assets/bot2/discord-bot/package-lock.json create mode 100644 attached_assets/bot2/discord-bot/package.json create mode 100644 attached_assets/bot2/discord-bot/scripts/register-commands.js create mode 100644 attached_assets/bot2/discord-bot/utils/roleManager.js create mode 100644 attached_assets/discord-bot_(1)_1765057157676.zip create mode 100644 attached_assets/discord-bot_1765057157677.zip diff --git a/.replit b/.replit index d9a3a7c..9dd5d03 100644 --- a/.replit +++ b/.replit @@ -9,4 +9,7 @@ channel = "stable-23_05" [deployment] run = ["python", "main.py"] deploymentTarget = "gce" -ignorePorts = true \ No newline at end of file +ignorePorts = true + +[agent] +expertMode = true 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_(1)_1765057157676.zip b/attached_assets/discord-bot_(1)_1765057157676.zip new file mode 100644 index 0000000000000000000000000000000000000000..91d7d7e84140870cd8148a19bb9e60bacc58619d GIT binary patch literal 44006 zcmaI71CS_DmIT_iZQFWn+qP}nw(aiMwr$(CZNE1Do83S2cPIXAMN~yqRGf;qac^d1 z*2ya`1q^}$@Q(uwHCXGvZvNj33;-m6v4xY7oue_Wp`A0miZUnwV9XPPh5Ub&Kmb4h zV8sXiuM&iRE1@&7b^Wh06-s&wmZ^HUKn4VWxg5U)fc6q+eG7OQ)LY>=^-{QBSkl@{ z=3idqetY?R>^nwHGz#vEN@9urq{r7bRgk5TChrLE+BM==+bI~k5Cs1N%Y@!*xqtTJg$1XsXHYAb-hapD z`}h3ML%{g|83LV&yMc|pwaLGR;+!~dm&JfEviqT?ej!C(EkO`kmGr>hr%{a%PSA9b zblg|%PRKe?qV@9;5x;uIcw%mMi9?L!p;|4cUh>`{*J`R^ma}XCRTC zq`NruMCPauOQnBoZF8P_mb`;}YExlF0=A?J4H>Q^m@f%vr(+@xw+0aE2_{Uj{LnA8 zos2|(*7y&`Xx2Q1NG;>|a=BCb_l?QZ6X{H1ejwQ43rAZ0Yg}}wKw6Ly2lV8?%mHBM z%~s3lGsY8sHwBSFnC6-*4fJf?uSVL>x<0sXH##}V1LQqZ%wt2+8-wnm@RF6hi3mmIhzlICk75fOs$ z_uec?Wl(detIhGXSli_toAkB3xT_9mpd=h(yjzDH*OyjLfwVCrIP|iDvFE*j|6C31 zI$VSQcp?G*zg7eOzpVyg5qW7j4H*$xWj!$!31JaB8{>a1iX!!GTkQ5fy}wJ@{Xo~6 zVWk|q#{B>Z9SpQ2sf6ocr5qa7uq;p12i-%n@9eK2L;|#%pr3$g=l+Py+a}y-J$XC; zO%tPp9Yu?}0@2J-Paf^>zkPneodV(<&$>lU)or zS*(MR09AHV1r?K7G;4s$eSh~{A1)b6bUpTc1&0AYH==;`o1PUgCxao^>KBzUK@iR6 zz^5^Z0j0_~D~MY$;tDI^j4Bi;U6t-^-w1B{KHWc$#37BJjj!)nZI6~9bVh0AAN*Kmg55gRE5;ek4>WLwYJNecY=xNZtYUV zm>$2dQKd+^H0$ldVDe{x#9P*bBO z(>5_!H0=f*YLltWm7^ZQQcs%@RV`H@`z!`go@nE8aqX|<;36C`SyGvp1k4xWWrqU^ zYyb`c*@WJ9R0j{wABs-qa02Bks$=9QRvn+Q0kf}MDAKj_i&npPmtC?&j?lJlk`1EJ zdP&^ffAso(-_eCP(bep`dwP5FX5;6~oHQiF%A0AmnC0RPuH3HAhJIX@tYaXE@)$InAg|G~6xUoC+z;775?0?5L zPWsIJOtE|4p>m6bh#oPhrfs0ecEs=wc3@%BawFIj-Q8JlQN@T|FR)h_=MJdZ>sBv* zGgBG$`F=Ex_N;<)an<0U1vZGMfQpN#9(mvvm7|XMDx|x&qVcTSq*t2U2CU_*_;N$= zUvRQN73rPZGuPs7nD3-%I$#Ubq=BSuz+lL#GY3t?^*WW0{c((-q(%ghYon8e(~E_} zd!}kr0Ac>0X7oc*7)s{1FG4E7msm4LpR`uus3X(!&OK&izqp)IVRKu=w zz5W5W-7T%p=v(c((Do_z)PL@Dg zddPv2aCWY)7Zgl%!)B$6m`@^N8t&ReJ+lRZivKn(AjonL9h(6cB;jO-Ll)!nYR{=uh zA_O)P3jMsU`h9<)V;mG{(iLmIoAS&sv^%<-%oXvdwly^+Bs+K%q;Zq+Cbf$U?g8WS z#klzU6*6u(2U_Z&Xa#?HFqA`1Rb;CtqThH<-s>p3r=#ZHZw+oRKDY;`U|k z*v4|V~?mHFJIeCUy4zc6z(?5T=n600VMKE&)`B8^Td7{aBn`^ZD##IFqzInKyEL6cc799^@am^Cl$^@ zFOvGHUYsdERXpAbe8|?HA8Piz4Rg~lblT|Uz}ZJxHTf5_xO+PHMKeQQ|7#7t4hF>= zLazhs2xE!n&ydWjoH4qGv=v*=54PDv_gSIYMipYB#Nhrt*=rs8sr&T!J5J7$Gu~Ws zd=B*0Fv`=q93Y(0m<%GE*#jyUj_xUJNwdQcC>8Q5EDInhCR)Lbzu$nox}SAq3sqsb z!Ki8C%G#iTRh-Hn5nqOaR*$9Y0p>#J*WUz@%~P)jL)~^3eb72<0P<@OeykcT!*!ck zVXO#fEi`DwtOHdyN$lGk=ecJ2xsWYeVnH#wOx1zCPAjm?U9)Of3EJ&;!orT_bNEHq z?+Spwc|YG?J~2Kv2yjj~O80+mV77d@iNu4kn(%RxR()0!#}1^)7;SyeCrm!p7Avf zbmZc{R$5csWe^lvH%;)Yd`7XLen?_}7Y!T;!1UdU*ce<8jM;gxLuDZgtNvA^5>mC64toc9@p!ImKa04V%n5?29KRz;@8s$n8TpQ|X=kOKIz+FRL-c){C=0rq=mbaJn~kp133>^p zy~~?2;clZ4-fGj@-g!%)wgY{SKy~Wpul6N@iwL)&kD$`&^`qvQ>;pL?m^ymZ4ODzG95Z`qh8TE;+v=LX}d!=Qz3L$toR%IBaN-T1ZDE< zUMX*1VnbhKsr>K&xCvxj6cH%ixK7^zs3nouPw|4D1yXNoT)G1qVgN76%I_p>antvs zq*vU3PRb%T@PPPV{o48W{QoB9-#r}CzfHeXT3860mwQg?$&b^U`r%giEz1AmPVjLw2aZ};BC{>Fd%#xGPzU$SYAU|0@)>z9J3kQkLK3oNlQ#ucMY$&LA0r+eX8)DsvUdS>$ zE})F7#vl#DfIq_!JCkZb0l9_&x387_cUEE8yGf!;pvP0Bh9EM@(y^IfO3eAXKhYRD z=5AR5y3)YSyhcmuXZ3L8X2?f>%znl$JBPi(z(2&29)r?4Uazu}w$VGyK2CSD@m3^f zp5=OZ);9V0czCy7-mz~Z@97~43-AU;YK@=H%SY#vX$#Uv2o*y1jMp>!hxN*aq9ekhK750+?3^aj7u z&HB>#cAsgAO)&%Q{fU@@vW;WVK7gCuGu2Ay5VPWMt-8BR2*1h~XCswm6oK7C6w_qd zcm~G>5w1OZK6CRhv_YQj=Rt5-W087TcukXe6d_=G1UUDE2vT`4lI~<@Pg?070Cwe^ znEmgVk^_&FaK>mD@(p+WdQ#^buU#3EJuY{F>oep?BAkHV>Ac*p4Uu0`cLdRhhdfr^ z;FO3Uq(@>fBguqIzUPtQ7-;EEENPhpxI}3*;1<1oX%^<}b{n_+k+sp(e@M|`r_?np zqpV^k ztt~{1sbrtH3ATHmAx=*b4JbADc->nRIBe1rYb63nahZDn5g4Ki!Wqu{ebDXXR5ktB zi6K>8>3<;XvF?Awgs;3TiIRciBagByqDU~#sur{?!a8~c+BTi^eo;6}e-?NYmDp~py$YgYc}~F+kQp1Z_eB7K zkmbaox`@DHtZ%=L<`a~!$DXdc%a2jefhvYJ@Xgg%N zD9V8|TfUj#H#T_v{?!TMG^om0c{k;PP07y^I6h zO0MM)jHX^0dC_2-5vryZUfi?4J1qBe7w7l6y4#0H>#0S6IKt%K?zcEU3{?}Keel2n zMb{(_BX*5IqQ?dRqLtY*k4iM`4<*3$Dn+POZ*>_ep9tI&34KA#*dG|p^?dw053!cq z*Lxm3M#iCH&QGIAmBN(FL5Yfl?=aWz?T)d3htFN)FKaYr;iGmd#8SQ!X3 z+hC;l@IiS=J-u;~Aha=fPkhAF)*;v|oe?KRv>NohC1ai>Uv1a;8&-du33DuY%6k<& zd)PeyQ*wkrJyzz7$|MRoc2z%Bn3fN(y2TQ^xr>8?H|GjZW<-?)2VpRNfm+|44mVgE zUmFPof_(-d$)PbdKA3M>X?p0eu-hlxkxT?>pC(@lby+Br*8kWdz*-)JVvtZ8Y6BrlmvyREz$w1GoK($7@t*FAnuGAC~(TG1H+btE?EE&`SX;q4wxZFCGeIC)olmJEoF}>aK_H{aIyD-h{ zH0khh@kwSwp~RFDuFYHR0Mj)s{#G|WUGrAV*6Y02O^QkmLD18*cbcr9O0!H_b3@b29{ zB2n9y>_MrqV!jEZa?v*y&AnuT)1O2w`#Jyf%KW|A`9qQe$6rHZ4e%LB53UG~ogcAB z+BgFGe&*X^VyF31c%&>^PV?7+R$y%tulN-^(*`J=G_p}HmF5NlRfvodPNXZSs=|pT z>WV9)epApmJKV;xIr`cA z)`<^Zoaiq)ZzLH`1r#49R8P`01`3C-cFJ@4AY{?rw4IAAngy#Q!ENVZj~Z&uz8||- zkLlhY{U}a;IHKt66JcLhmTeiO%u+&K3I|kzFG`T^&?tY=gUD3X@agCHhJ{lPhq>?c(Skwo)*h! z@R>{J6{#iAKxPqT-?U;N1VWlE!4|n);MK_t#L>`7)nPb<0s5m=@JFvfH0ZvBWZj!6 zc{EbuWrp#AAvVPHEu5akMRxjbduLK$M-h8Q;*TT&DLRrFCJjeNJ)O#`+`iR{7;fr*7zpj0 zGz_4GF$`B%SLS26Mt(M!pz_#EoRxdd>|m{f5YRYKIH0J*L>9ty63K-m8ejr@>4xK; zqi`hHM^s1Kiv@K30}u9c!=_ve70W&38A>Wk9PPidAo3ekL+ml-mrc`n&!v>9`csy9WmjwpHEf!`x+^j*Y^VTJ>_4|_W9BbWf^hg# z^#exje@0Y-Q?ifV8|E5pOq{Ut)ME>yBE#zZH0>^)H2+k}hTPZL_S!aIaV8=13j}Fd z9Vwwz(;$N43s0S)qVz0{%Wo6bm$NA&O4u)9sYZ7*B|%m%hQn!B#n)5o%UCCTqlH7d z^JHZ6M=KB+LM&${8@Wbg@TlZl%2FUVe;uxVbcQ9Zo|2E2BEQ}N`f_+ajoHRr_jo(m z8?*XtvgK@};q&^3)rH^x(U%+s5P?qg2bamH08Wm=C>>~%?ZhR=zM6R7 z*0u?Fh53;;K6Jr`eATReeaaO>sD#HGgPItNgUUBf*sp;8`_B%Yp20A^{|h_pdwF;< zW_d9tMm^jbm|(6m-z+k$heS|jcZQd5&@xE+@K3i6X-j{u_T7SUT^7lcrTx@c_L%!N#2X4-$Gc@VWR|otQ;BmsF)dkkhmI^u&I$dgNA7vd?#HgCqoAU zQV{bjZ0{+SbiV`}-Ef_}F*TYOXnhE+)RkH^1kL>iyeKhwaH1yaTXXsZmUeMmHfcvt z{>oY0MIOWRkB_T2iajAQI+K>_r_;WRMP@vT+SZM>%}LyKYV0?RAj$~)-qC2L8&l}c zmP&Q=6tlpQ^&MCQdpc=y1CjTfif=6v;}{X$91(V9@sJcr-7Yi;4?7pHlF&oWN##k8 z7PMQKkb5&%_%2LLSwYSYh=OX)~e zaSf-vu2^CNpA*j|3{*p? z_K_2=B*~RBK^#TrY^7nvEmfW-Ro@md<1o2*N|->eJnp({=;BIQ(^xI6#fI8Z6(E`D zWsb4ykNkP_F$X)bfcbsCk~kQFICXRVx%k+){InYRf8ZAhx`~IJFFJ zvG44^L@Dr3nO!l3CiO?o25LLwUfjp6@cNfjyxxt!O+y`?0u#(*FM$|4wT#-_YU?7X z?4k}Bn_ASr&^cJUa~R?qQfjVI(Wr)PA6S7KH}fhhbN{-gV&Qmv#;~NIAiB{L*Skh% zn;s63ePQ;w22r@_fOQB+7S&Fct)a3DhALgg%|ifGmAMUTIq2WibB0n|Ec@*J`4rd* zWwpTt-wd0Mr{T2kCv}?6vBJ3ECRCmdSWo+42=ESA3C^}aR_e=rNDTx7e%y|WsG7cH z6RVSeRcS!5v3to03?uXTII8{O$fD@MhXAA!zdZIT_oa6KY+^{g!R zD!ywD1~w4vK|v_*NFt83qQx~P=^(yBr)1-WHNSgNPG+VLIH%~Ru zL2uYo-=r;g?J&8^bmyMSbmk#QNL?z5n|w4>LDmE1_*Sh}S#kiWC915ugn`w~7%e{( zP5_LkbW%2yq$qmTwH^#_tnS%>@P3;Et=4o=3#a$0b~jt0b3tZcYJm2+qBT(VY8$w* z8sDPHsmXu;ndQoK;)kzC0hcDF};fiZTj5^XDx*7T^{dH913;+dD=Ovt$@UWG14|wn3WzZdXVhRX$W8 z`-DcBgGng8HT9jE`&fAI2XaZtv>YA+URhh8SLsuz&XpHxs)3-+79ChRGk6t!RlgKucr%icl^mI3&~&lOkmwqq25+UQ0k<#V zk9xzgA=lk<>KZiL7|}azu}225Su5+B ziQlUD5nB|B*r2i$zpG;ov6ofN(wGezWLXUQ0j*kix~;i5(R0cPy6))$Rt zASDr+y7WsMd7zY>=&JLd_8w>Rdx8kK+)Ot>pqr)Oxb{3q*ZIn%a4!m!@IM)5uqVwx zpN`?mI{4+YB&kftbaAm|3m+86XdjR5AuNY#hoJU=GNzt>IK2T0Qz28b>J$*;$`!khNRx;A4P z;(RE!b#oq0Y!-HsgWv93YWS?PG(g5(rV}EGhjR+ zV25fk$r3|H&ks1cGhhX9PPwC7tsJJjVhH9K9rirGr#?xl6%Ef%~kVR2LA zWtG@DQalutwi#D?S+0H7QS)gvnYZCRX03i_P8F45Q^NC0h~+L(yYqZFk>BoE1?MYm zM_}1DCx7cVCkdLmkbT484yYYwK#Llx%L4l!EZlnIA;=?nV?k5zbKhq#k3E{f^1jB5*Cc z$moS4|4T;U)py1`_d=La)SAJJLUDDs6^}Ret_%J*kH-V*2!v?w*`S#oj|Yngys=#G z+46j~I=3|p=u*y5Kw=-(PT^%gYQ&Vnr|FAJ=#Um|G=v3_%v)m^Jicp$ zEkm*3+%N{-R(_JT48B}0cYAYOnA@33`7=_i&uc@Kf>6_>s$joNy9O5h1?%u3yXuYix zS==VH{Bv5GNW6WDis>$hfG(csR;>8-#_r(Y+vQ@WLz+_de6N~R-nLbuKmvd1UUpLSH~|D}cw)zO{fyzlKhtgz;|U+&ULwm8F?Ua*u6!={hQ zfMWt6A*H(tkgKMAp~3v?@jTa{Ty^EyeWu8L9iyIJ7MEGH$}IG0XL^i%(Ipq_q&ceT z28biyrcTGr>vkf*d8>LxYup^XS=KEkEP&Fv<+ROJepx9&eOX3pERcjrB8j0Z0oeMB zIxpgD?~wb`s(l?|ppDtq3Otpxj}t=$q@zy{bKqwzX=t{uIBzo`FW{+Sf}^@*+M_ zrwN72J)enB)u*Z7_mR#`p^ivskvy%!cV?1msQj;kxtWuoVDh}52fEPec%;W@`-X#y z7nF`4(JHRs?w*b30465iORnJNJPm^Z*=O;#a$L<=X%nq7-81!j;QF8IqAKm^H=H{T zXOh30F~cK&cvC#q(8Fx&VJk;CpFi&aSy#1B*R@?Pw85dVOfD7MpI^>#waABX-~8Ne zb`xGQA5)SY(Cs$aI|DH#6cU@{dYb3>Z8Dno*3@6Rd8|csHU~YkyLqZgZs~0K^5;|4 zt8D_yS&myb1oD}7HI}}kA8KyGWiL;T>M9NceFzM*-(AIueDznRre{1|zkF4F!hVQd z^*OuN-*J=T2K5|mVvq#Jmpi=iG3&HQy@W%2v1;OfZ;Zh`T`K!}gw^S-qr1Fp(S5(- z|LA_=>$2Sv15GX}%WkoN{#jW6%0jZc@6wxuUAiy=la(hN@hs#-Bf^FcgM?K;Qlc9{ zothro@cFh?;b!L~FQ56e>aEp9AUWUSB}^X!t6uGlGQ;!?a6Ia9IhxrXc7eZ1ZH&4Y zT+h35H(9+pzMdD~?p>y1E=uqrsfX_CLeUyfXY*3%;ZrAC^cBB`!e0~fvvOo3_RPvG z1>V?&W0eEjeY7NEodd8JqL=F8f0%fd4TQjcSbVYrpje}KnAC(VJ)kzzZW#ko_hMrXKIfbI@kh#R7wEHt6`w^Y|Zi? zYSx|o{&nM@v%RwvUxmqQmRWttr_Pq(5{mL{nLn@_zrRmzs~pAow%m($=yf-nzO8yM znyRWSF1$P{^n_w$?_WC9DEGB|me|mybWH<*c6g}sAj(|P3%v27zAh2W@aMBa1A(eM z3G#J3&{-x(bFDb?lhbH_+kTq;tDA?P5nFtR`L~Uv|IfzJ-&X)gDE~YBhx%`gBO^N- z8v|Qor~gg?4nO?Q6yP7^|6PmopK8rbtnL5Vev141i6WJ0TVfG}o)1b%A1tXP#seVs z34_&s|0V)`Vmd;_iER@(JI{`=8>}CSOKfsF9QA-9Qgl2ZT{As@ZlH_YdLC0)P09qK z_2oZ4-xrk?w&&LRZDDC(LL3e%A*wufO$tX&uT7crH`vzuK-^z*=5iVGoZ_b{_f+Bg zZ~F2a%NSJVI$X1WbbG!4dGQV~sj8)8jr#xqc@RT-1|k_0<4Xq!T}1@lH5y!55w|SW z05Jh(&MFI7rm+Go&VX}N>Bx?Ul;H-v7y$Zf>X1!j>kK9vOdiPzmg&KOO)5$>@N|ho zzU4VBs$)Z#r;YF{_&%@quq2pn&v?Ll1>@2TsM;|59;0yd!of9nbA-h=m9lD&$EUiMhJ26cD_MoiX9_NU31T} zn;~Of9#Sl@G5a0pKnSgcq#iuGTLT(`B0j$KAY8nzIYY!FAa+&r&vXOGW15NH(o8Qk z?{eD)4P7(FuO>ZP(8v? z&=q43UKuFiv)&-0`AG}y{MAbvmr!3|3xvk>_Gpf;Fw~ixy?HofGpiiZI!34?^JDvo zsb=%%`<*?ye_lWRS^-c8C`WMWr(Jo?HqII`QSt(LkM z09&ooS!<6T_i7-A5Go&;!F5Lp(?={I)m0v@G^{p;=D?GO4ui^$xC@f;hXc;F^a?qu z!A?aiCE<*MHU2mGr3 zE}4IzvoYIELWV_J&4JXzKB9Ofqt3Wwz>A4*D{{#<_uzIr;LJEb=ZTU88{mM6Mjv9;diAzZM)Ti zwlk}gE#)roMm2C`d((|99XKT64`hLKREoV?$8j(ZgDaUGpKN4QW#T5XA(95$@V7J2 zar<}#nkhyhEC~FP!^xn{6jtQo(?xdZ*6R$S&_O1La$Zl2{nKFUvdl z3`>rQ1c{WWNC%*(jjgT_F32=n{6tM1@nw?3Jy)G$l}}$ zQ5P6^%ntZ)^9P=9Fl|D;Ekn~GlpmkUNJiL@-KfEtNUD`TFS7N#*XPfoU9Yd2RvURP zAv{X4dngy`Zny6nSZ*09j=6D9YL4Lsoj|i{wNm84JcNJ7-=04AXj7B8h=D@mQVJEA z65V<U#1}C>I=H)JF49r7iNBKZMY4(78jG+ByEFSrJ9LsbBzBsF0C%;qS4G^-@HO;qL*A z*$x?vi<3wH5OW!&o*ybs=40`e4gu?(GO$oZwhLpO=*$Wri}0~w%xE^yOb0%T#WAdqu;$U&0cv1PkA&Vc4Cu}Sp>3@+JWb$d& z5k{txOn{bhgHuGV!OnAONlUYLe?rg5Cp$}B;&n*BU@?oHoqy_y=bBzIfL31$Ukwu5 z`F@3saJ6dQgKPGE%Y6#^WCrhyXx_E@X3G!!bGcJ7zNZ`!h(w_9XB5^?D+3GPdDJQW zo!He~x%6xBEFe*G@&3*4=dt(4X=T^z03tB=s+t>FxvupgSs~|&_Xi9SEKdqK zuq|yRCVwxF8$eVVqj^NLr!Qk%BLW<%(?q$Kl#P%W!dg`YNnCAZHhg4p|9 zs@FoXAXuE#phc!@Cv0y45##qd%jcg1`b*pbK?N_o{J+WR=XeGCkH76rBozGL4^TOn zv-iCprx_9ivDEzIIMC0pVy{1 zHd4NZ7${?pM5vqndE{RUjG+eonR3KM3e7k%Zs5S$i%3Ty%tDk!2X&9}NkfMS?hI2( z_w4}y5G+{W#bXWR#ccv@?^!Q|>jyXy!N8#+1PJs){2>`I2YB(!*n@;mJSfvMgd)YU zp@SDjMHv}=&ZL7I-uZ&Nvr+*3waWZnV@VV-P-5t*zush1C*B$~U&nNiVPmN3dtH7{ z!6`_~p!qW5byolMn;6XQ(N5RhxP$4*+Xsj9!<|H6Ll5VkRT zOx9zOvWoNlBHt<$=f*Ylt4aAeHd{j%OeN~yBI{Tr{$`@l{BvA%gDsReCF$d7s3BX@ zMp-$-jsTcKfdTmlmaRqN!Hxw35S|+aVjn7D!C*X)N2Kx=$DanuL5w99Zy!1UQ#k=f zfF8UqX)Qt=Nm)Tm^UB=?Lc=sg3uahH3IHvIq&i#XVFQ?F+K@@kjy&7%7|pzmvlS9g zmC-uHTz}AkPdGXhEKsf>gMllMuP(dfEvS4mxgRwHQP^LG9;_0J@n@Qe@fd^k8@LW- z!0P$Q09zaxO7kS7JMzIB4K56?#hMC^B#jDZcHwDcJ*M$^+sKqXv_I^nb}xX%er)J5 zb&Zn}A8ZHB--LWj%4RD?)xPZ-2rQAL0cB*MsAM*o(x%{cI70Mq^igbh;jMi8cLAVH zIFoo`A6H%{BtS@svLPr&Jkz3K|4wl1zQmvVrhz~uJs4G@e0e4loK^s#luJ!-S`2H- zI09OZ-)(Y^vM>rsiuWek%Isrnmv>{z2D}?grRvhLf5*zNka%UFWIUM7TAm+I!fnT z#o~q#+MDKTKJRgaXKWE;CxJ1 z%c^X7KRsV$#ctC3rImRLE+jjMZrP}axpQv4k zEp2Gl*T0S3h^>kzEf_26Al5+o28CXJsyS>5PwuiO&v%xs)tft13wDiYS|f1Z-8e1m4iiKH1#~X3p3iH?WX!_v>8U3_YhmXT0vwhn)|2Q@ z(@JSP`Sfh5j{zAw@|h``Unw6>jpP1cXY0@B7c~yy{+lIyOHB%_ZPTs~KMU!xQ&k*b zR)LplNmAE9Q_p7w&Z|P}>i09-Yv{g6NQ^Fw%T%cx9va0}(6kJ;+~8EY;3yXth)if> z_`#d3lpWVIARD)i8um#|GMI`xB$!#MW2YL%BEb=;y=yE%)?f|jBv`CQ>_&Xo9hBwd z`fzWqZLo&AHmQbI;Ng+G$U;_NaONdahUce?~4_;-FvS{xWY;c7Z?FJv{4ce=;_17g9)cOjx56YQ57 zxz4tV-tvoziDm%Vay{Tnzbj{gXHtd%sxA(>KU@hr^w zjT>i(p{#ynRK~(eA7B+HK(r<&Ktx(+Hu*Lj0Kes2?i>&@(<0R=(75LpVy5(C#<3*X z5#^|qI#uaDAaCvg(8X6DDR)I*ylw5`Uw)!w?gKTQF&roskzu|q0w0+^OUJqb^9FcI zbcCKyO*V*qhcPqEr~)ukqLEi(1gOLnJXiX2TBW;!2apM*x(-G6J4{lgCd>biu0P9a zh501g)22+0V@^xo6xk5wlF*DN!(vySCr437_{)A&GxChM9WkUI`!S8{P_lfDXZXttHnBKAi)^62D@EF}w2w+3v$mFI`-udY@ZpS&$} zvg!~=;??GlpP-vXP6xS)#c2`Ai(7`(TI+>#H2sCM#|`7A)X^y{VHD| zxLJVlt+l@2q>3}aXzA2E{rc}Gt7FNOJkXW9KZw;}PfJ12Evu^uG5cFd>bURSzmh+) zUTp;wr02qJ{hk;Lvsxl^=et#O_F3EnVMu8aYHIwKj{OYflOA*y<=HnJnOug|73mIC zZEzMinw8;m(E1OTHZm}uK2WYv&?yH!{K(8hm5viiO?e|O5JK{!ah*`6h8R_`3;PS< zNIz~|_^*EKo>=>`p~7sSL&+?hc+UCOHl~aBkSYoBwI+68F+^Wft6gV=xI_=%iG;l# z7bp4rK2nqIH8u8N$|*#-_HDh&_Nbu2u6scEGsP-b*?3x&Svsq@5)Dqxl5wg^7RjmVI~ai5Q0)w4c;Owr2e()E2$kDP zufoUd%q4Hdv?DcMGF1|nw<_+$hDSZdxA5g4EWIv+on?ZU1YW4*-B9o-g-kyEyMfB9 zlT5yiAl5s0+ROG)lD$Nm;PJ&RmN%tj$DS%43+9%dGhch8bpW?=8Ipvv zzJiaa`suHqhl2jUn1u8{`FTeZQ%4ggb6Q6`YZIsc$tO*!Qnp*72-~xiy5Ru%Ad!P9 z=tdBsG+dR!3?yC#!U4-dXkoZ)oukVl62T0US1`u-L;SG`k4Hf5f^(nY%!$@RvP{$! zl^wjby4|WbRp?uLQxtl*ogJzz?i=Px2;}a%^cJ7c*f@dbI)xbrWpli4+C_YC=C}9O zFa=$!)Ls`bJT`G{vUNlr#}RRDJKeb@kD^WYPOhOnYO2-YLU|h3kt6bwt5%~24&hmt z@)Qct>K&eC6Yh3k0OjNy3crRCmBx=MgAg{vlKx=6W&hD^YIJae5H9Ve*IZ@D{lk;r z8D9LL+_%1y$~I*TD)!V+VC9Z9?|NKV6xEbAW$OP>JuR@B%qCdhB3ShhPZwafLIoln zk1eD6+|dKzQrx6J`HQxUgtxfa;t+eTqaaf~12hS7bH7xIQU5;!r`F{1F-rKZD&s6)3+iQA%% zj_|mwQ+}5+y4FpzWtwxz^@Kb(YuKE`r^;BD+vf2a7^vhyozWmG&mpBb-;I$O&gi;} z2CpWs=1z0Zk|9Ox_valRx$K=zn40tICE&zU?^&~zJe#g3yW3ge!`3fV0$XFL@^vXs zTRo3~;m<_%v}=*NTJb%Rdiex6N{S^TnB!&z+T~IWyW;BYc&GWE_ULuRkQ#E1b2+Yi zCn-+3do*7^Yk$Kg7frRuaS~jj>ljw5+P5?&cQ^E|=`Su@3iFUj*Fb6pRO3M<29som z+xc>-S~^&=WY*t|m}FE}ERP`TE~zOdsRjWbB9v4jA4xtyF*0)HK9L}dg_LsRj2=pPF&HlL?4LzPQJnMH zzFa_$p;M%q;1{liy}08sc?YV`w@PAD0Ywh_A$;L1BHrGeRHQ7h#(e)WW!6ucVvRHN z`}>dXzfnHHFQpOr7v+AC{};*${}alcOq^*QO$@AU{wKn-)#U83MNoP_l)7pv%yHYX zql5s(z=~}H<1^)lkL8OMnASb4;B#^u%uJKgmqvFG#8FEtP;uhp+3iIGZKx!|u0ODng|3hUy81 zn%3l_HaU=tyoQ11r~R9mpf5(4{W4&C(9>WwcK_fc=A0-0B<3Sv@$a|u)!n^--oi4c zVbkXvU~xDFVud#p);-=_;$oiQXUF9s<%E(4n=+ zV$pZnB({3R-%`J#az7vFG2S}W*J~KcOd~CV>yJ{pm)UsR;#2a57HYd+)7a`(OGZ?! zF_i-|IIR2Vsvklh!K6@TIgsUQGd$i(|Gduxg`#15lo1(;xKn*OPH|01p>r67FpCN`OG~qtOuhiu0WD~) zc)A^rEuc2}0|}+|A!sj%{+?YBcPwA{Hc{B(>GWDD;WZ33%Ixxa#KjQ@ZkdQDv#DWN z1cW5;prNLqw(%C%0o;0l10HwMfCLt$v&z?(%({wr)g;cg%_%e-{#;rhH|NJLfLW-b zbNsOq1k9ULmfVtu70=K>4T259>r41>x{B#z>_wrI8$6G{FOnd&l6zlhW3%vO1X?^eFF66lKAn=#kjOWYb z#(bPbo~2ZhOx|lbfuYV4ohx(TZ!EQOEMdtn$sU5DZ0rI_N|a(>JB*KC1(zSa_s4Oo zrYq(^Un0m`@RoIW2Nm3~yW(S!`|_zX%6+W@phDv5Rr|>b&B6j|66|@z*46+MckTB$ zYMVYA@!8srQi;pTrL?(7gmGqVf`<0^O=QU0ODpD-sDmp#t&p(>Z0m0CpT2$`m>wEd zqZc%lauPK;uG^mEk(oo9;k5Sf>PE*NN#n3 zsok}=Mw@_HO;M$1SyZOYPys}YITq`iK)G!FqB8(1dr%71Aw7j`_7e(HUDbC7mNY{L zOksI4)H_eA2$!!y4n~8*YPEH20XlB-jfA94*lTZ=0&->uJ>K6$aKIEBwj_au0ZHui zKCKzg`8}HxCQYTKdu=M^#X*M+V`gBHzX}zy3&sXiED-L1*x2p#Ko@N5+FpgZ5nF09R_SC{F5y|rS#|1w>Stfs zazv;H+e5?*yhy9)g)Kzs{fF$8Eqrm?5u7=tXOdgBLkooP}TvJWD@0iD@=447w~HG4Rzl>6jlEH zf+#DPpJq}qBe)}^k-kMU+%UZt;aTZtoP(Oy2!PSl2#)lO1_{4T3*bMS*Cl}r6IQo~ z#k8X%glv(cS#8XJ<**r4H_oCx#3i%|?v{Z)K@6VR&356&=kCuwT8x*tD;kOZQCYgpQ#ea17>**9=#jRjw58xlHyKf<+;k zM(#QlJZ}c{L6oy46sG{8*%Ec@CL%^sJHQP_;5R49Wx8UK=7}MIYA1fhEyCpCA)J* zH-Y2$Gv=sy>|;3f2-mRp&pJ!H6W;&#MML>W;c`k^(wR_w|VZsOY^> z!m0GHG{j<9?|$-bR?E84GFwb{+~YEz6lq61(UBvzRKz0aB#mGm8%)Vl5ckVjV92bkXeHiYpHP&24r!uMa8X|MsUyLx*kVehE=yqIDp|yL| zTNWorOZU3~?F;vRe=Ts2gwpHvctWvjZw1c0Kpd7K9HHKR3jX};C+Nx)las~`E6}Sd zfJq;vbHVealJ%H3Yh?#L)JK^6#9V#W)LmvVRWaS&?MlMR9gLJBsA>StuRX?1>K^De zkSxEPFF0%p%v7$SfYH?8kZi0!6YIw!-*2#$=sUW`K}$uG^ad952nzHWZ9@hHFO})O z6=ZZ#ZMCIAR+5l~^*ZENRCK_1{Oz}8o7%H;p71oD63{xgf4S6We)|Kx5sAUDed?DO z&$Zft3Rx*FXz5OSMa1oyw_Q-DP(Kz_B1{7;ed({`DYK>-v6`aa zZn_Qpex}1G^vJ z+aUJq_0Jqmr%8OwzHQ5fj&k9rwd>ZF{Ppfx;>)i?p!CclL+6N>I-cLK0gHLL5vOp6 zOeEi{inO^!#i?5V)4ZqdUJP6PgztiX3*Y~g0rCE?@aTt6Dr))ek-J= zt$?T*;)a@zM>I}RUginbW)Zdn)CS9P%+UqJj;o(d8vDw_Yrzb_6{k4Bs34#+$y2R% z1jyvXP5&ko!0mJO^CW_5=8k??;H|ZXP=q(rd6Tog2xT8fQIEo~!xT^>$OlalRcNK7 zn=&lQ1jUkuW8+vVuri7%Wvb@qo-X|2C+dOEI(gd? z3X}x5@g_+C={y~xz5nF)C*9Hb^!Ftx#6N((Zzb-G=Gzu|oqxQfTev^wAQZ{j#g1To z4@X4b*{5l*MB`U zw-GE|>?~3|lXMBhnV5O`S-!ry!t3*y()z{v6bJ`c6SajC$i(}a)wLuy-AuO6msq}$A^RC43KwN2%2}K4g}Ug!!t$aEkjid5fBZ zg%2D0$fjYm#8fJKyn%rV&7xFHM$e>~`B=kS$_Mbf@93``zrG3iJwA>Zxi;mo59Bhg zdZ|v;LENF_Lv-!4+lCwAW+r{EiRQ?Q|1rf7xa6lZ{}fel|G(|^|7nVGHF2~s_4t1p z+W-DDR;q5<60@TGNb)$|tnrG-4t4zHwxFMTrcLFN=oX-*=TS2x+izyePtM5e+<;;D zb!y_~cCCjw$7|^N_<$Z+65K+PW&yfI`!i3k)34LViP)Fio4v;gZLjeP5f)z-q$FZo zfyk^wM||pifa=}~kdc^s{fEPjZ=3OZU;244H6)7KNyDDt;P%V_yORx&=H6}RH`W|K zt~9t$qhbc)6?2ig%?44TwQAggi7JXNhF`>DPLnnI7F5u=yF)nWUXz@R3!{4+!@vd| z!~=8s0KJ=v$Ntch)UoNac(fA(62$f@W5ttuwQwl@UB>-FK>0Y(jmPTaCa%Eh(+c?} zjzHV05EZ?g7x%7hynuOQ#)Y)O-Q`MXv9@&DMu6r9(d!4dOubtf&lw$qn`VWM=JcTW z0v5rBt{!8Rk`6D1USYe&i$sWQN_`7P!8sC>883jsfNNfRPkSNaLZCvAdwJFR<-e`H zbZO?}2(q>#Jd?2IO)o6nLV^@U49B>K>6?G|Rj~ z+%0OJKS7u2)A3h2U`sy8f2EEr{s!#W`7?%?%On6e$gyOZicf7Ydd{b zOO4+fYz{}Gy+X@$HUFFI!Z-TDbB`SiSFxv>c9mRQGnUI=?BAgbSNs5QS`0(uG!6M1 zy~}r4^T(nmQBir1>w`TKqurvb(g+i>xNi*auUZ@$Noii*;Fi&Lc#7ekN5^jRR{HU& z`?BB%oOPeJ27#hWX&2y z?Z*x)Y@EJ3N?buX?_Y748kbiMtU#cT6#b@0(Kq6e?l^n|yI2SdUJ8tEuFaW581Z69 zxMawrxybiIp3YZu%;eJJMhBdcz>_(;6gd({ah3;2Ga2(KUuPh(7eVzd;lH5%t5t(; z4=jNK0RXu9H=#bO@!zqR|7WiMSB&|u@B(2;Z;_??U$zdgNSxMF*P=r43@QXDk!E0E zS*=xvfFBZvr5j1zQ34dFl>~zGMHN3B?W|M~w~(DrPAC681n9HaL~=6Gsfc>~!0T(b z9fx#dJxC^8Z}5RD@4Uva${PtflL)d4KuiPV+jl4=WYzTJyp~r2vW1J)Uf?oP!896} z{*4PAOg#ZA+Zonm0RL4YpAlx`K3FV99q_3;#Pz|j;EE(rGeMmJ z&10~b@q+;J@M%=QQ%9e$nd8>-)Sp3`Ge9dflzp&-?C5VImd;)}piKIg-&#=w@j$4- zBGUlgW1s~wJwJpPR7g0;Qm-VEDONe{Iw`kp{OnqvD~GAt4*~>G&b)v6Bm930F-zMUtVFT?>__3#*dD5)}7- zfDGs$x?r9Rf(HeP>xET1Xd$u9D`GEeJ$f!!)U@=xYsU$rb$;KYV@L*Z>jXuHTGZN| zB4678M(N=o`PcSRht}JlkSLS|=xDOodoG4@VfTy!rBlb-4mcbHo9hEIh_@+#ANZH$ zWnj6us!gfO@HZNKGVfC?be|_$4+1c%h=?%gi3=h35y#RpM z3>)p}VP_y$LBehT?ESE$Q1msW9f{3#p2L!oefcTOi`wY13sO-#-e&;NewmB@(lQT@ z;C9b)IS;TuzvJCUPFl+9nzFZ0>Lg8jt`3U~uQPM;@EvxY-!AJZ``ci<2$7(}>Emwk zP~T?U5@#=Y)H{n|)4%+%bjA8NMMFOB)SfN}U%v7wsPtpC2ad1CZ8PhhJe%)$`wMD> zuAlsA3=+K_N2eiHHWI@(Zj=_y4wiRcunFtnn{cyt4X|YIa)0`N;G}0tQOra?VJGe< z5d9C_>7OkI_CH#T|A6&c+qoFi8QIyITA2Men3OwC;D=U=5pw+zp`E2fs1Ib2SUHAz zi&iE<1(ocX0pkryK3u$7r~&tWiCnmTS+{fi31|29)Ja;@Je_j|hwM(ik8|QHMR7o@ zjr%6)lo7wYzU{47WvPQ18)UP0rct`Sv?IEzcz!SG+!=tsPjF2KMcEHyfCh5TaGU7T z>97+FvhRb+olkDiAd)r(*njbtcvABBTI#;;mhWuS$ouxgx@Yngf~k@mJgnN^;i}nj z0vv;sW)b9^)~FemZ)RPwi{?Zp{okPXxxU|lMyXSBW!O9eBdk6hq$9|_3b`WF$k=GH zPWC(cg5MlrqNSK6IV%-l!BJ9?{f$AI_P>oPX7c3I40l37I9xvg{G3))AZ;;0>pG4U zUhv*kP<9<(DJ?e=D0(N_@x^ODFxpU_;32*JEQAZhwQc^$X!p%)L%1@v=MsgYy)tfe zAFxb>E8$ z^K8a`{$!AU$$z&{%+ZlQ)GqiRGV#A&?vIBM-~=b=-%!fP|9H73t|qq5|G9o~SWExE zAk6)rl=Lu1-inE!Jr5Wwz| zNRvU7i8+|~#DL+BaVl|CEo>?A1I;Ti`WEri@skjUNTT430rxt2BYN|>^|_CyG*j$@ zQw$K^Gm(g=G&cYQ^0O%IrTB=nw!9N3$!Q zgTGGo)2Xgrfq!nf7iwL4{N%Hxt66!zwyil1*xr9Y*{*YhcAQWwm6ue^`>lNgwHP89 z$6Cx%LRpC2kCCEUhF@J%vz+RuwY{aYQ*BqR&$xblX6o{K^=0|Fe|IFSxk=7K0upR^B_Z=FC_whG zh|t7EjPiOc8ZP##$fS5Mdfnfa59kOfBF2oql^TSQW1f~K-+sIN9TEW|#8cBE5ED?E z-TO?P8U<)O*KM%N<9h;tJWgR=C20*!Hq3Df8u4IGbkL9nObF#@{awHry<^$7QXkR# zZwR?*>=ewMw=BE|2)__HQWL(=VcJlOZCBx6le5=<-D9o$Zrb9zU2%6IQk6+&e5&E} ziFWx(Jr*6;2?&a%-v$@HeJ>pMfWHA5vB`6gwC*nz{ZlbgJHucAcJIZe04ivu`1;$TTw*2cbjT~z}x!DcPMD0=?xkkf?AkD80nx&PtSC53c z5q>$NRVY-p#PmmvCq-IUwHb3biHDOGQ$b;e8_F~Q2K2yivp^p+C}f)k!Hh>B5to>R zL;d=b34>Kig3Y$bwMYTN!%l9T<02V3S%a_NHiaPyuP_a*ytv)wZ6o)7UjFQ_C&i9 zoc1sT@O_-l%uaiAUXHfwp|7L@EeB6Jb>i3N8)X29Y*CoC^XAx(eU zKQ)1VhBNW6`?8L|2d5Inw89RvmSd;PI3sqbvQir5o@iOUmhOq{!0jDTUD!NaT7}|e zIu<{yp2E_h{V_{g3W1pP=OD2MQ`1Qa5xTEpC^!&E@;S#PsmX$zMr%Z&ZpX z%!Eo$sFP)R!J96=w930hdBk+0D#XetvUulNpTUz`2_Uc)_TW8*0A7Z%HS)}!*^jqP z=+HIvf!WVJBgzW;GHfZ;c&0VC?bf@ggX0@YP{R;;sWh#9@zt&|vN1GH{~%Yv7P?41 zkXPT&zzhyl9gGRwssW`T?ap&o`JOvfOBWf+olNDP`!48~V-do;S}{F0Yk=J_UtM8! zy#Zp#2(_eJGSP$f)9dPpEJ_Er=)S~m?l((rO>j_cjgov@f+(;N4=Eau*bYNMw%=Dc zL9o@X*Q~Z+b2iVADZ9vH|5IO^88o=BG?7tE#w%3TssP zrRH%iyTY{$R75$_`(~uc{c4e!Gh66RZnle>if{>>;m06p85bIY=#P_~9#^BUdcI#5x0;py%)<(wXgx*fh&tIoJdriNooU`y&f(0kqD%Joi=RbsHd5(i z)Qt&Obe#NUE{+ylkvWdbC?PKyI;Gr1iGuzuR{b_oz)OTIgx{7C&CMAfpiQ-mI#g0RRra0CDe2}`}JSWh`t|-Ru+5DKvHcK^R+O<3g9-U0@NWcs4X^YA~d6#sMB)WpPC z$-~y@zgn^Xcuu3nhAs98X7{nu$2L}9eSV6w6=@U5dNL`L0ku>J930d@o@mE+0y3-{ z8*@W~dIrCpr&=Q@5&=6c%)zfqp;yQch#?$T&JGe4R(>CYc&8WmosUqv5XPCpKu(xq zwX{we3Mu(spf`e>jX)@vpx@(ZktA=TV3%8}ZME$)wRWy!{A|$R8@LH}F z8(J}fJ!B--fCqsi6ehpdWSm`ZJ+BY9*YuqoJQvqk+}JpLm^bmGWaVPOvX>P{_Vn!; z*1s*UU)=3ku<-bMnm_g3oBbR;89bZVxtA-ZCyLBy{n6pIvBt6#P{FeEKAWHE6a!@k zT%q;B>jUe?6IRH@#0vIqUw0p-wh(53gtD8^-G zBU6%Y!zjoRU!bq{wyDn;~zhioN*z52E8x|un(|}1XL1IScIR5Abb1xnE16(MNOc$atuCuBM zV-B@f;7Qf#2Oo=7w&l8OKFBc;cI$xA$s;ViTa-73vD3#0UV>mq9SR*rPBIa9>O+Z( z4uq`pr;+4%8OIp${AR08VgV2(FcKF6k80*mqj?iRQksWrxupyKC~bapRP1JDl%n!2 ziM6No6w6JLBMZZolkg$uc3G;hAyTuHdkO$1eFnnieIRL^11RMblDxaxS}U@1#^IvI z4M6Et{Q0f}0N{xv>b>+@0=rwH)NXI4N&}9EWx1qs^wT?nIH@@s+%D+H)x`nFcz7Jf z+r)aU(#Dare_E+~z39T`s`kzTgEs!}%y1HV;izRax8lA#2-Q8sKfQ3HRvE`>NqV42 z7D!7XYbnW=6(_$|t$|MU%bCU^5!v6@%$6bFU4#=o%};W25JaTuTLd)NEL)%mBtdgS z1PB$P9Q%~U<-slw8-+va>YV^AoQso!iqeZMFZW(Su6G6hKvXW;wn#eV)M3VP3~Nig zoPB(_sC~PEp_hmuDbo2{Zq;F;iUbqCa>GHcrvRL6@&~tBY&BrZWMpQ=bj6~`@W#T}(CH_Ep z)qA+_(U4BJd+Uvf5IG^+B$B&2bUG&syLsqLxC*CT4cITQN@v`Hq8(e{i9wG_=lu>Z zKOA_3Ra)+tHm?pPugr*@GU9Hvu)R}LmaA->(J^nT?#x64rHf$>AJ%`AMCZMv4LcCC|7-D|t~?yAp%aGwOu$E^N3OGUFmIoG^zmKz`Ze zzk{tS)#<8MagKs4Ne7~(TAjRhuW>a_pPj(yIfQQgU=bVYX{lnVVSwf@{nw}7&OFdN z+B$(G6h(uGhh^eqvGJVT-N@Coa}B#63b)g)hmCfW#vPqEhkZ(`k%s|Y%IFiSRHhkF zc(}g&qC+0++lA-$LCBPKM)U4~GjCmUu zZ0ebyIaM>;)~mCovGP92& z80YcR1~)bjv8#8Zk#d;2sdi((4C7EItAx#fmZO0VD4<+2y=Bhb5x)=2X_l_Jb%WEw z?&da8_qKQ^kS7HjI&Knmzh@owPWiW`;c+=Dk$%9O zuRaqA1A26!!&ERSxKt-Bx$Hzkp=RW4MO`^V!No9-*-ivZ&434_J}M|ybuW45ENb+y z@<+dFtwjsq&m=*s%dg+YH!k}{au9;4>3Ad$4Jl&k1A0RE;dZufcINGL2dwShM+O3q|)x)e#l z9ZE|7a&lkvR*4`J4yk@?%zQHUmkPJa?`j#wy(5y^z3n74P{?+q*hoztm!!lctPz@Y@`G#e`~vPymaZBz?xll%{Y~8>l^lR&gf}2wL7a+ zKv`mK$Le?*o78GGU;3tNczO)Vkp8?88xj1m7JxtPXREMoTVa? zk+2diEc6q0hm-T{N?ugrM{XkrNf*GM)Z4(yVB)49hrRkcd*16>vl6372S$?fn`O?$ za3}->lM3dONa3y{S47BJ%31l3>KS_6d=!H!+1#0zrvyu9iLAF6G1ce{jIP&L!Y<^b)wqY5(-gsW4+=9+CW1r)^cLW;K{JetLn5^Y#SCfL}9y?UH#JAy)v$y3m!m)@=qjFw(6@LbjKm8TWhRL5bI9M}I zN?=pzYru>GJcknbN=J0Z{m!?qmhNt<)GB76rPEMA+4GD#@83TgnE#Sm>@VHdcYel$ z5jg)4|_0!V`XtG`Khhwt6Y^=3m{$DX{O$o+Nmu(A3$zi(If&~a&#BlHAJ zRZUe@O;2~e@AgM#g3vE&G+WF~sq1`PX ziG9r4x+#13L}D((+h(=~rTxAh#ZnHA>S$FTvn}yA;E2CoZtp+OzFtOn)p`bQ!+*4o zG(12M-^V%Iv^q>I)6x#JeZ1TczZzly)4cQ$11ufZn2xeKF8vR{o;>ZZESvU$9`YjXv+Idegk#7ms!V#s}T$Ks)g_J2f{K z38j3<%+VkHEO4<|G?5EEK>UX5wi%?4QW)U6Z%Cw1mkRz@sl`_gVX2e0yde$)la0vi z74Z9oF%bgxZX@?Z0w!(TbdHun`#8DiK643AN~j|Q;_DS?LG7bC_b3i+sZKl5Y_Iie zt{@Tl{4ReO_P8#}%d1P#B~lzL-fSY34Mg<+MbB?F0x=g>bu zdk{vHj=9~U6HEQk@ApPxb2F^mV)?eDw?z<1=}D#*xA-6RIvdYBniIPunO(z;&`i_P z12Roa193X^b3@x-lj%Kuywy&a`H?Ye?j*b7;30BSfi$n)LedM_h2h$>^kT<1*8upR zKkK~&yXZ|HtYMCjx>dxy&E^Nf3F8JyYU(;++ zyv6wgr?t+5SL`{PNRZ6bz@N+#1Q2b~=Rj3+LOJ7q` zP&F2XxSU-uutyv8lvey|+Rb;B`Ef5ZIw;+c`33i7)qLy^>m2*tE>`;c*6vusp@e7+i!HZyEN{D+3SKoz@-*oCdMV*XxMP++$|Eq|0Z%q&MzHU+W~Cjg7c z<)kYs`e+9Z2JjDX>)~J(=X{WPvlI+rk zb{?M43V?ug3wat?llEeP%$7f0J7cHO0sGy3Q}}Le zZJ+biEf#b1o%2smKowlan!Zi~MT>bWd4Q-&z5}v@+ zq({pRL>TV%PJQj2ZC13{q8juFjRk%9;j0h#onp?h-Ffq@J4Yj?2JuXX#RB?Yt;sxx zdF8@_nTQ7vJwpiCkHoVH9Jn(P9M@Ww?>Q##yG-^m>zY98bKwB}Y4qRPldjgOyqM|| zF~j&R$O|ZCCY=sD1{_{)j=2*7+ft=c(YsDr`hJhn9~6fEay#S+xEFhSXKmveojmH4 zUsdx0F59i+7VqjnI|D81!g&fyG<4AsCn%4yZf)gOKI!4sqb=(dFiKakUL0pR#=0QW z=kbNsnCcD+C-2qmya&fIAOf^L)tvcD;6d`xZm8+&_WsnyO=^nyxUs=yOEi9Fs#xv@ zJlVDVEQjEF!4F=}QWMWXiG>UXYT1lC{%o8kBV}oH?I_6Pv^{{rfjDL)gEig?tx)`) zjK-`l1Xe;M27&hsqG>mTr(armb71&hYg=z3{NZY?mwpDMVco8=Yhh-5pg=+HTNp1p z7SNgAR?7(*B>(jK?e11PQQ&T8zFb3KO-1;lRjs z_56|3Sg+OkB~tY4huJ!){5c6b$rrC-CWN!!e(hGJQB*U+k@^Qo>0OD*Ql0@y8$mNl zK&SA%9z0_?BP3}wZ0(fFtlyZth@mXbKmE;mb;7$z`EYV;ZZkde5IPzs@22GFb@P#Al1LLr}IHF^v8f@IDMPJU{kEN+m!>EH?q6JU&qk?&O zj8j(V!+}B!UgzVQCwzHBBi#dvSEn!uz=nE}=pQ@j;{fve#cO@luJ$6nMpm{TUHTL+ ziL}V+jsoHwb?8cux8(Tc#AP4<-PBgcQx%aDU*Tk%_31k4?%zr+B_x9J za%PNEXMa|%a#Y6K4Z^gQ3KtM<;I0cFBSH@6r+#&TpDpT2K>1W=6=V2gyY>pF`N-D6 zeM=*NycUi+XO(;IL##kM<{ba+l&${){b+UYhOwV*k1w;e5AX~L(dNB+3(0vaPA;SG z#x+)cD4SZQdsL+ySyOQvWw^{RaW*_@8>bP4Da)w~gJWQfjn=csHqgy-5{4vVedAG5 zMt**J&6tlLdG%e=mDD}{tO6RQYfH~7{s7E0%ky>KEx1K~-K}Ar`uS-aXKSgVyv)h9 zXka37ng=z1K5tSNK!i$Ihx#j6LFm=haL!|tpm34HBX?Cg01+fOWccAxg@L(TL6|n| z`k(>SLk_zHU*W6=nI7RQjO^vcrW*Wxx+@zz`~%A8W=l?Oi+IyVaOTyt<8h6nIgvxN z4_XTvvy2!*rhD)m>+-c5+!6@zP14vikQ4WjO4F^pmOymWvvyyCu=>&(ll}bnGqA|n z`4`SRsmM*nU$O%Dj};!mEpm8+YtG-s=lMD_gmpsB}fa5!4A0+pvV@OvW)k@G=>_|a+VlDln1U;^4!LpoX9T| zxjqOSwwvcAYG=#IJjHS0P02>jiPN(HfxrZC zL>^HvToN7r=(i^1n@=g=t`LKt@t!$#bnehGd8>VQrJt#;Uti@cxuhca5aN|QJR5b1 zoX~5hd5Yp z9no2=$OfM6;NFVx8aJllBWH>=S)K>88;bSgH&%c>=bw7el3g@eZKLJd=7)Fo>$<>; zze4t+p_vLu^7mmmXUNv@JEo1lHPsXLgsD-fOE*X>e7^0~g#cq(c5X(dsA!Kf2?Weo z)Nit&*e~x+zOz1tLq9i6=AVG}+hmArC=bQYJ(-Dg&U{4>mB@RuHF7m##Hr1)sdHR}kE?YyGN`Y%# zlY&s=v~^NxI${UCLK_mS~jXWy+O3ICR1Vh zu@~N;+;C2Psh&A(QTxX=TwX2z=H$mKb$s!YWDUJu5YD-#P|CbI_Kbd&qC@|55YcD2 zX0~LzS5>Z@{`9{{|JszUuH+No7 zv=cjSYlvlQc(z6v1Kh>GYGQtJ(t&MR{N+7?-XqQ&mRMTep^5~$_1vG9>WbN!aaNIU zi?wyFjPL-&+gBuIp7&ms?mpbM%oLS|;woKGy>)9JOWRU1mz7E;W^VZh zSDf>7q7?cQF-1SibG*9ig-o_=I7F4zZ4$BqZ>A`=Jd@lHs|%?K8glv`MqC^7 z5pGJ|V80HeTLe(!9U0eIED;xd)8&$eCIxZuAGif}@lh6=2frVM<5#lcP{g-pIpVk+ z?iyi*kX6__Oi?QgJh>K*(~4dO3)?{W?CSJ3aOCr7we0dM50WT)0bulC<^8I!yW%j8N-H$j<@Xb*9d&QqL*-gbHw<{WuM%?zcG41DV=10B6zCh9cz{j+ zs+#Kax{&)KxrF5h(-S1%k{ZR!M2njbA9;5!GY`^XFlC%H1oyFP2 zoJ)7q1+`JTcBY{w9zU-PKAk>h@dp{zE+=TLcv25quxf`@ElO^YqTW z9C*#JT!@Y5)e?6s#B=b218gwFbLI2fjyFiy#U2(h~C9*a%>&y zGB!H`h>iQ;1&HszY0ZY{n4G~R4Zk7p@TCS#mQ(e| z?+cfc=_D%jdi~-CrqkQ>J$^ZQX%Hs1Y}jvv*Kbv(qeixGL-PJw&oO1I(W{q}S2&jH zNpIR@_g?YwS}&zMybLQObFfD4mi)=qH?BnfDRria@v<)CJ>dL(Y_7e>7w9Ddbttqg z@2zFxyy^jCUqTNuI7=qku8*8*>e+ITzQTN@G(T;WTmjkin(~rII z=s3ljyK(|UNCaY?uh*4+S3R(I5E6M26>0@TL=Inj4ppxo$F-Fxl|8b`bf8cpt(*fk zIQ6oO)n5^E=^~wQHWwbwn{}P$SmisZ-YQNvux#@Q(msKH`%Ud44ANfmy+u6iLqywF zuiWpbaA7P(JM0KH0Bo!1S{`lA(q)3_FA>8$EqO)gL0~Yg8Y+RgHZ{xAz%G$p`5}>o z&L-zjeg3L3lWF<(^~|N%HwHIAKa+H=vy@q9L3aEm$q*Z##O%)!yH7DTUyb*w;COo> zecbic{4DnE;n%LsDe(904%N$W-W!Y$YS2XKIm}y+?WvO?Os$^{;yi}n#&-F9A7*dd ziB0kVX@@3dyjnhS$u`*3M=48s)1V*~uNypApBTYN?gSf%_Hrw(& z#yFQ{omT3m726rohh)WLU=FU)KD@bU6hSF6a8UwZTNT-Pl62p|?-IE(UQn7|AC@O! z$q`o;L(n+O-O`M6X28Vhg=z-VoEv;?65Ez!~r_*h}{~YmsXvNmq-HGp?_1vo}GxO%5r* z$D{{fjQAXI)3maYv!WH>?_)Y(UE3oQffMK`-a7ktfG>hq0nE<7TP002wNYrRuk-4_ zm6W9wi+@01572p_l%u8qmW7=A)XRD>Dlb8KF^vQG;QInFO!6VF9s=0HxC**Q)igLx z%nj^D0jG&Wl^UrCxw#gOT5*JIRS|U2uzhL0zE56z3F*yD!e6H1dnhNqm4A7j0JTt! zd@DwO%LMOj6&Tv0G+_A|Dyd|8Pjj)86k{ zudfvrR70qJk_p2kdqrp*^@CsrJf`1YoO|Zf#?qP$n!;DtL$lQZGrRPbPVr(!7H7>~ z40tM+vfC8c^H-^16(em;rtl#igwzB{ZC+f6gkAL2v2zac4al}Q!s}14 zbN3*JvjFi|om{;h&^2$&C*=a-FdxySd~^c)g36U+)ZM~Tsnm~49iL7XXaLKGWsI5o+^&ama zzx-Z1o$mLHVp&I zAQ&@1BW(_<1XfxJZ?}>$+1-|?+L9#~lp0YhLU1|6!dI}rU*#4rkRkfsU1dz{po`D| zt@eH%^w;5sM`Yn2lj`(*YckEG)#;gM_#V*B?-`x3wgH!iHk-g1Y|fEky;WR}ne$FE zh1B$%%fnOD$H%P2{l{Atw1@7GDCFsMdFaMlZu?_NOt#E?L7)eKnPY?4wZb&ci;>Jd z?dyj6lXiz1h&=}o_<=UaAc?$V4?>udVOIn<`R!xXet@hd$NHYNjqlOwuUGJw*Ui@& ztr2p!y}u;a6~0@^Ki9^$qMFSBu`|pBTHjELT}EPT-E*xM^5k6R>$&m9n?;>ap9s+C z(#N(%(@v79ibOSCsU!C*dR53b0smZi98~G-PbxqWKydLaBs<86MH#`{ib?jAw-CzK zhbEvvfy_-a7>^4fbmNH?JwEJ4W92{(IjEq-E6u$0JY>|`RkifOLddty2YL(3O1Twl z?!yK4C4hJS*Z1RRNc-}0k)tmC_oE%cV*Td6)33QI*4_QOZ|H*tBMDi99O5G%S>*G* zMl7UvRuu>J-qfY(u^<&?K%kziGv-VQDpFJ?jw@1zKQKnx;wfKA7!N}N(jyY&31++g z{0eIIsU$ctAdx{ss!|bbjD1$k{m(e8bDHvB_w)PD!K`#~0ayQDUuOYTMccJukZ$P~ z1Vp;K>(C|L-Q6A10s_({Asj%uJER+=I~0&E=@9+T`Tclb^wV#%&H~nAF80hbvuB=X z&A#t1zKh4Ors0|I#iC~OuAp1ex9?Opyt{k*9#npr zWn#rgfIUB1rQAT0(|nNYDnh=cvW)AaO9K1~Xzv;$j>mry(ciKvf-@rMg| zdiKU&i<`e@f1))_`l>^%a?1XsjNy{7Eb^1ymr%>-tD}B14 z`l0N_x`KmSgWsGh$1UMC^ro061td!7F}I2r)jRTH_-%q>EU6p`bsc6k2MSdl_4BE- zRC!1vvZ+UPZyDo=XS0hWZF9X>y6|PR)vz+ZID8Kfs7<+N z@OpMBtGhy_deie7>fg*G>ub@3sO(ftziyJ~_(728u6Ui~&pH>$muPzii|De-7{iW6 ze-$4sfipEe0uU4i7^>5C3b2~8AnBGG&&P7JvpO^7k}92|J3O|cBg*b^sXFYBK)|cq zvd%EHb50k3deY-18QaC(>}k-;*IC6;v@-!*aq`R}|4MsEo2Y*jeQ1XyBFe+cK8Vhz zME-p2&6K!#hX(eIseGxhSTqS~lA|?6)qt z?O%ZwbO(2yv;})v?qo}=-#p6RZ(v@~c8%8aYvIJoUh?NF46gVvp&Q^QM7}dgGEGhT zq_$JsVkkj8OJru;{bEuo=upmU$M&psy3acjXumk*NE8FN0UZSCY2Uwt{0#30t}&K= zhWK&#Qqm8evMi#7y^hf5slQd)az)-MS?gNPbXKqq zqQAC_in&?KS3|C_(SIn_dldagAgarUx?0!aYrc>#QKNG1-4Ty=xSkxi`aL&#!In_R z{sGV3!rvMpiHW!ZN*YJ=5&k|e)~hXu(hV$~cW%|#37VED95;3XlLSH2Px`3@x!hJC zpExg%y6*V(9_omYNP3Kqx&3gXtr&_*U1L@zbRBIJx5B7)(J&=YpsJ#-d|}K8nJ$T3 z5w~Q(dLDTY2h&?&V6UF3gvr#aoiE62nt@G424D58-fw2uU)j_v5B|31%-4s0((FTz z9GY?K&02lRMez2Au1vfOugQxu{}+VTtvhxRL9h06o;1@dS{V(JKNYhsldUz=7DphQ zE=f1v!9tKA6it8XVa&Y*uU{d{w|Ejn93SFsQ&SSyDbd2F#WzsW9R5kM*#hLlw?&XK zUa;$<&#WLOpoC|uzN4Mm_I{@8X`sDr(i~0n5k_TT_NYqw&BicoMJ3U`IP&fp3hV%p zlj*^GquhMVcbFxK^E3GS`=zSfn9Q<+rh#c~RNue1_91=s@n)aRJ^h4^8seF~%%>md zVP$&~?X@R$e9f3R%E~0sd})=u!8~rQ3#J?!!z&6 zN6^A58uE%J%$cm;QzjaX2-YV{s}gV8dP_M~u?2<-Ze2pP#~XX3l46SA?n!9WDRRKH z7;9jX`zY&wX&XGgc404%HH})pvhp$vI#!xxCXc@=A6Wx& zbpJ3(?@rEruZqo^Xq21`3vPt#(AV#=qbudZmhhw~5}$^zMIG*Ysf++$$Bb8%3_3Kd z`qa}j$W(Fk`3eZQE@qz28<8oP!B?VWWHYLhst;2LspB>hR-+EeNqOYa!YHBnDzD>t zt63&$Cq)I0oFg9Y0ETfZFt7rs#Mq=e&W)H{t=i#$TUCZ_3D`^U80`x1^UpZO?xx20 zJ64|04r)QJJxS)*@Msb$y*t;7GTvO{4y+`HFt#;dyXWX zdqnXr*;SW8(@gOZ9k}c0KM7&0=)N`J>*i7sXJbmLMd&!=XItdH(<8D*8mK>pY`Z8N zU<&i|PWVbl)*LX;ZpSS#hIAoYbAf#(G(D+$3GI^gc1K6VIhBdSFP1vDh_3i4)Tr}p zYypEz$!=y{yI7vlwg9$dx{=xQaU(T5oXB|(0WwQHW8TNjad(<*p%etD&$?H9v~#Z^ zxEG<@7*C$tHJ7(R3M|<->P#8tt{wzjw|oQ4K)05bNezqg&2Y7f}wSmyvv`6TE$9Es#phYSXH5 zmPSwZ@eF}0YeoxSI{;Z$xCH+!%}H3wXk^!#PZX$W{<`q?yHPJ1i3WE462pyG+%z?l z7+-%8$MeRO5}cP=(gplD@X+O~8v>%o0jnDeR#ji#pqD&SPCdM)-18WD5=rB&QCuT7 zo$mThVS+4f`m+TZP@{I!(V#VRT~m~3d*ds@SZ)eJm*II#dg0yVk0JOCA5SS$itigi*)R2`LwCq(iE(vZaAw5X zVj(IrzJJNygF#~Cx_LEM6S^l$P8Sg>s(0Egox-uOt?+8+EitQ$*T5+Nr%O9${i7fQ zYFZ0UT1(ZbZ<(_Ekz6xG%p@K2NOCvw?xmD-Y)NvO@Yfq@xi#kw>s{DNd$-d$Mb>IU z@l@-^qw>{nDj_#tRKl|S4H)MNq^GF_Z!>XYPR5u@RceRbUH12;W|ofkY6Qa9KhLLM z5&10iO?86aTe0#^_^91!iYGV6=^K%7Gar z>T^eet-b}L`hcx>u+YdwHe@k+NnYHZiu$#tPJ3Kq*P6Yv^pGdr6Whet+kQGE)gsy@ zVd5BZsa?}kNE#aXjl%Kvl5WdmuXU^!3XxVIaFIK#7?4iDdo`a(h5lM%-;H@h zR6x7TP1@4Dh(df!=?R@@^g`Kk$7Z9)WS(aOps+6nlDCWh&CdFvWG+shKRR?ZeK9~W zs*C7FG9IE#20Qorb<14MT-2yi++oxKY=uG*qE6C8+=k&797E?;9|AS#2(aDyE>*BI zqn4s=u+MCS1uW+6;}7;B8ut*dQ#B?Dw|7)+`iAPZV0l>4DGI45lEvFN*$Olaw1c+; zO1ATEVOPUsmaCM6MHCSUH)xF+VTnbY#hKV?t~3|SH8@m6U#D)VgqDy=>)nW)mN+#W z!1)T0J@F$;QmC8$d=x!$E+p_ZnASfKx~Uv@nS9Xq@Rc|Z4o0RHew}$jJ_Q5yJGLmF9EqkiK3c%bKE>ue`E!krXbZjYrI>BoSK{7bmUR z;(80|JNDc9*s1wq+#!n?IMQ%k>Pk9y5oxr7c4&m14VzF@#!6lwogPS4?FH=;=)lJS zJL*hkH5oP*Y1yBcYNUVKO-3Bu`{qU2_XUGs+t12ETRY0wxzqNTU9B1cnB1gGMAuuYI%L_$q3gF=G(y-nV9t-%CEi%=wpn%=0lkFP=XIbC^TMRBm0JK ze0=P8Ks#s=R*bbs=pgovn>Oa0Q(hkjoz%e@XV~16jhRhpEI_Pog0y`EK9cGX^5dHK zqHFe?x8i5|M-Dbz^&l@E?gLmm*GY)a3W>pO0Z%0Fyyklrv4b~fxMl(=>IqA5zgQGL zPstIDV^cK4G+)JszS8U2GC+u~1R%^t(*VliI8f=gCIS5Pm|4!szFbHb>F;q>$M|S% zjvD34>y>-T2sh1F(A>oMFnZi;57E~J06F8N1!JE@_FkKk$- z&MEdV8{d`QJ)RL~M`Ii1MqxE$U(>E6NQgM|>@&sCCAj>pOO`XgZ%gL`FoKx!f`*wE zUv}*-p`Ejo-2AH+j|B)LnDNnSu%l<7(v2O%kCZj-w>EeQ#5V)l$0#}2d2XBp%f!mN zt&p*M9qei6w5@^gy9t1?gmK*1)C}+c+r$pC56$QSn zm>a$)cInYgs$wy3D?Bh&WecSF;1WxTKgJSbXJMr;yJbls*|Dv95t4}Rrl{p9%}WFi z1Yt+V+`esm%@eR{?jC3np&k5IAo=mhvNcIoqZqZ7f13(SBJ$lq795naxOBoq>M}#B7EO_mneBGLMU09 z%Hc1+4B7#a=gcXv$|fN>H{p1qY(ydtROeih!)mZu{9-Qwj--JV`Q^?o7%4P!gvUyS zatYF@>aT=*fUoGu*e`o;Twd&yN;71leAtUU3XLv061bi8=l*uLrT^U?)bITB1>M3_ zco8{T2#B)WhcD><-sq0^TQBr~UeNvb-CVGizKzn5-6GTDmRnjJp7a`G7-95mfbzzK zsC^qYy-+MtYGniMo0{G1{tvi5{TLHV!5OI;pG&^mK@cdRrb}30^yf=B&@z zU<$lLOQL>(>cn_yYQ-=`7#oGZ@oZ+NWf-RwJQKHg^0bu_CFg|s>noH_D`9#5 zkyplEa9;biueJd!iYA@5Qs;AbeUiHbZUt1+jk;uJwZ`ZAguEzYAcR|{lK>{M1CGWC0dHso<0{Casvv{{6kOb*8LO(`FpDZ$%J z!C^i7WI*XsIlE+Q-sO_mx07qTEdOP(bW54>PkNx+t!}@LP zoQ<6<9qt?1!Q=LKeFLh$Z0clc4(o^+1uHxg3v$ZQ57pL>_`70}*i=AUY1Df5*)%DJC&(-c3h0ih?4Y_kZJLcqIdy-Tf zvz;OU9cPspIkU4hBo4t!PLWPO@w9y1ubt%;pLTF=N*X?2NEhA=s_P5+4n@2r{C)jx{aYL&h`w7?&7FZx2@_Daq2UZD!ZLNB12oy>$i>2~eaIk-4=KI*9|qG`ig08L!TT7G$mC!H%zOSP$3 zZX7-3#h-Z1awHrD=diK`li5mx#@a>shgsdl_}*zXRHeZ z2NO-*5=g7De#DcA^vH*T$a#}bt&B?>E7rC%$1p?DkJm@0eH)f7dA>C~oKvwKGRHjF z!}n4>$vjo%v1(K&SKzP~Gus)}tUJre`aEMDmq!L`d7l+w59QDxQ(kCjd+?&q4}st- z)cB=rK@wg(nJ|;sJQF-OA|$hSk5Y>jt2dyhn1NG}ZRmwD_Q3sV&X z1Zet0i>Mb-SXsHKO<|))6`+dL$pYwb!%a;93Mzfm0q{8v8R5#`WV>W?QZE>nzwb!fS_!N7(Esor-fJZ)GTn zxcl?q7sokki;7J@2SAPy4h~Ml0_=7zvYQa#b?LZd5Dgusk7&{`7d%!|HM(kL)mve= z< zmkL+cB_Rk=IzW&p$DY__U3~vS$2@IDOVr~T*NyZ1ld!}eucmlmS$$A0r0&F?#%8B9(9Vn!;9_DO^szzD5yqT9Xa(>QJY4UsNgU*j#?pC3BOKfU++c%Un*a@#6 zZRS>%8a!rS3Tc#Ori}lvA1IRhJ+WySe{VHnQgD)tahOXUSAg0pLXj1U4KdcxFOhdLe9_*)F9*ZussT0_6W{%Z{D}5%9o5WD)hlSSbL#<`w0Ry55uGxs7oKa@~z1v4M9UcHWi2FCLmWYjJMCJ*NUMSClKz) z$dCo{h@mb#exCel^0s11hpE*m*>Yr)GAIH;hf3EFHe_N58K)@rS+&jpvUE#%<2Rlt z?BZ)&Ivb>XO_m7s=ub_z+EYY%t`h{dxp@KSaHY_kSEo9K1gSpDO>lsK-j|ib9A=Ng z`Y_GZs$K%fkyucMpWeKfzA|&o2kd_8=jbrU5}n~?4mpQz-%Y%Q;Svaysp-*ushXXV z>!$A$aR)Q}lAp=0RppRTt}Q#+(%#Bz_sIzi5Khm8pv1$rsP|idW$McJ&01+w{)p$t zR?Eef_CaJ2F(`BzwqvWc@uLT{HJO5e^vODE6=Tw8@Dfi;Kuhr%E>8H_Gt$kc+BaJs zdtIzwtzW)Xxln0)YRxGeDV3k`rh@>97i-a{$KJBDMpEYqY(4Bb|1O~(rDl6#a_dWj zVs1TW&FK`SJC}Ahc|q|RKq938MKb=QXX4uw12@@Op>qY}AC_YK9N-*on+d)IRv{wg zQCkf%Oyh>Sl;+*jvU{)p$mD!gFu>@rM0HB13slJ9G`kLXq%#(@7EotcT z=`fx5%-p3O)(aZ-M5vxGaTbhGMp*q2G@6m8OD6D=OAfA*u({}EgqsG`VuD~bkwA{( zC43Qs?xYjC1fac!)EU<79Z(`=3dc2Vg7_)MzOVF3b)yuEWLZkPjFq1Ym%5kKhh$$U z;3UinN$b{$^$9npv_TriDrZxBa(U#z#c^e_PqH;+rk#V%+F;sE1ZcE06P=9)?*J-8^l{toLA61o;8A|n%pYIjB zlK9D__UdFamqbi1Mk^6W(tYjNc~x}c2z!(w)mLffvxGrKJJb=h63LLJrTQY^$L+_} z4idWcS6EEN$N`v+qsBlN{?xDC@IZ~Ag?QI;Hs78j1c;qCK0apifvu+-qNAhlRd z!6uQ`r|i!rccI64vHYk7zaH9W&L7{ihC#)`vge)LQ!R8iTevPrX=$jkkC~}wW& zlB;7TM94jd8R-$}BRQD!>^NVav5uRDZ%({=#;;(aJiF*2)$uJ(qHWS9sW!kCU7P&opzq-GF46c|56tna0vD&K z@53RRZvOFJ*IV}WT~F7t9hJNH2Kd$5f({9&yvsX?+56I&&hD=Aif>~^$*Vu3CL4Z^ zxu_`Uort88uWPci;qmio3A1tEz$vnk?9!LXqd%YmdP1Y2#L8XfOsYkj#9z3a=>Id7 zP*f=E@C6maEL3nYgnWbr1rnM71uXWj?$pSjs`fJ;LmUW$e*)`f**lrg8`-=3w6~5r z1FKB@M1UIx!4-8Iac0j4^v8an&*Og(5D>rs*542ergm=sXm9{{%~L-3KtB5;Vd6Bl0i4%n!-n9e?OM_iM7M%zq*O z?k4&W2|k-28c_Xu-f(jM4f)^pb-z;|8s31-tL_6B5WxEv>R+v_erG>4O#th>+y^kg zT<|aKKN{#f(S4{)4Aus{4`6_R%wL%QtqT4- z^q~~#ujf~<{9mAdkvM)x21}VfR2%#?T^i;8Nxr{Y!NR5wq2OciP~{FRRCpi2fOqP@ zLcy2DpJWT~iQp}N*SG_#R$KTb6 z{;izg99<25&GBDtqW`a)KZT9{O%n`z*uDPiDPQ$h*q?-s?vuRleSf&<_UkFXWA$s2 zfA55U$OI4f!^NOq?`siBe`JEro&Pk}|D1&U+;#Anf^p#Q_W@o(`WfKkD4y~w?horp z{~kp!>fs~_yrOg;prZ5_)c;&xy5Dgy=J!nhy}f}m?a%l%)Bl=x{hH^)=NItI>pp-1 fkePo1{be4eC<6l$*@A!|0)5?ro(!gQz`y+;Qz9YX literal 0 HcmV?d00001 diff --git a/attached_assets/discord-bot_1765057157677.zip b/attached_assets/discord-bot_1765057157677.zip new file mode 100644 index 0000000000000000000000000000000000000000..ce8e0811fd1832bd770500f02c080619893af5c0 GIT binary patch literal 37140 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