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
This commit is contained in:
sirpiglr 2025-12-06 21:44:38 +00:00
parent 3b82e95e95
commit 02e50ed478
41 changed files with 7328 additions and 1 deletions

View file

@ -9,4 +9,7 @@ channel = "stable-23_05"
[deployment]
run = ["python", "main.py"]
deploymentTarget = "gce"
ignorePorts = true
ignorePorts = true
[agent]
expertMode = true

View file

@ -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

View file

@ -0,0 +1,211 @@
# AeThex Discord Bot - Spaceship Deployment Guide
## 📋 Prerequisites
- Spaceship hosting account with Node.js support
- Discord bot credentials (already in your environment variables)
- Supabase project credentials
- Git access to your repository
## 🚀 Deployment Steps
### Step 1: Prepare the Bot Directory
Ensure all bot files are committed:
```
code/discord-bot/
├── bot.js
├── package.json
├── .env.example
├── Dockerfile
└── commands/
├── verify.js
├── set-realm.js
├── profile.js
├── unlink.js
└── verify-role.js
```
### Step 2: Create Node.js App on Spaceship
1. Log in to your Spaceship hosting dashboard
2. Click "Create New Application"
3. Select **Node.js** as the runtime
4. Name it: `aethex-discord-bot`
5. Select your repository and branch
### Step 3: Configure Environment Variables
In Spaceship Application Settings → Environment Variables, add:
```
DISCORD_BOT_TOKEN=<your_bot_token_from_discord_developer_portal>
DISCORD_CLIENT_ID=<your_client_id>
DISCORD_PUBLIC_KEY=<your_public_key>
SUPABASE_URL=<your_supabase_url>
SUPABASE_SERVICE_ROLE=<your_service_role_key>
BOT_PORT=3000
NODE_ENV=production
```
**Note:** Get these values from:
- Discord Developer Portal: Applications → Your Bot → Token & General Information
- Supabase Dashboard: Project Settings → API
### Step 4: Configure Build & Run Settings
In Spaceship Application Settings:
**Build Command:**
```bash
npm install
```
**Start Command:**
```bash
npm start
```
**Root Directory:**
```
code/discord-bot
```
### Step 5: Deploy
1. Click "Deploy" in Spaceship dashboard
2. Monitor logs for:
```
✅ Bot logged in as <BOT_NAME>#<ID>
📡 Listening in X server(s)
✅ Successfully registered X slash commands.
```
### Step 6: Verify Bot is Online
Once deployed:
1. Go to your Discord server
2. Type `/verify` - the command autocomplete should appear
3. Bot should be online with status "Listening to /verify to link your AeThex account"
## 📡 Discord Bot Endpoints
The bot will be accessible at:
```
https://<your-spaceship-domain>/
```
The bot uses Discord's WebSocket connection (not HTTP), so it doesn't need to expose HTTP endpoints. It listens to Discord events via `client.login(DISCORD_BOT_TOKEN)`.
## 🔌 API Integration
Frontend calls to link Discord accounts:
- **Endpoint:** `POST /api/discord/link`
- **Body:** `{ verification_code, user_id }`
- **Response:** `{ success: true, message: "..." }`
Discord Verify page (`/discord-verify?code=XXX`) will automatically:
1. Call `/api/discord/link` with the verification code
2. Link the Discord ID to the AeThex user account
3. Redirect to dashboard on success
## 🛠️ Debugging
### Check bot logs on Spaceship:
- Application → Logs
- Filter for "bot.js" or "error"
### Common issues:
**"Discord bot not responding to commands"**
- Check: `DISCORD_BOT_TOKEN` is correct
- Check: Bot is added to the Discord server with "applications.commands" scope
- Check: Spaceship logs show "✅ Logged in"
**"Supabase verification fails"**
- Check: `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE` are correct
- Check: `discord_links` and `discord_verifications` tables exist
- Run migration: `code/supabase/migrations/20250107_add_discord_integration.sql`
**"Slash commands not appearing in Discord"**
- Check: Logs show "✅ Successfully registered X slash commands"
- Discord may need 1-2 minutes to sync commands
- Try typing `/` in Discord to force refresh
- Check: Bot has "applications.commands" permission in server
## 📊 Monitoring
### Key metrics to monitor:
- Bot uptime (should be 24/7)
- Command usage (in Supabase)
- Verification code usage (in Supabase)
- Discord role sync success rate
### View in Admin Dashboard:
- AeThex Admin Panel → Discord Management tab
- Shows:
- Bot status
- Servers connected
- Linked accounts count
- Role mapping status
## 🔄 Updating the Bot
1. Make code changes locally
2. Test with `npm start`
3. Commit and push to your branch
4. Spaceship will auto-deploy on push
5. Monitor logs to ensure deployment succeeds
## 🆘 Support
For issues:
1. Check Spaceship logs
2. Review `/api/discord/link` endpoint response
3. Verify all environment variables are set correctly
4. Ensure Supabase tables exist and have correct schema
## 📝 Database Setup
Run this migration on your AeThex Supabase:
```sql
-- From code/supabase/migrations/20250107_add_discord_integration.sql
-- This creates:
-- - discord_links (links Discord ID to AeThex user)
-- - discord_verifications (temporary verification codes)
-- - discord_role_mappings (realm → Discord role mapping)
-- - discord_user_roles (tracking assigned roles)
```
## 🎉 You're All Set!
Once deployed, users can:
1. Click "Link Discord" in their profile settings
2. Type `/verify` in Discord
3. Click the verification link
4. Their Discord account is linked to their AeThex account
5. They can use `/set-realm`, `/profile`, `/unlink`, and `/verify-role` commands
---
**Deployment Date:** `<date>`
**Bot Status:** `<status>`
**Last Updated:** `<timestamp>`

View file

@ -0,0 +1,22 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install --production
# Copy bot source
COPY . .
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
# Start bot
CMD ["npm", "start"]

View file

@ -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(`
<!DOCTYPE html>
<html>
<head>
<title>Register Discord Commands</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
text-align: center;
max-width: 500px;
}
h1 {
color: #333;
margin-bottom: 20px;
}
p {
color: #666;
margin-bottom: 30px;
}
button {
background: #667eea;
color: white;
border: none;
padding: 12px 30px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: background 0.3s;
}
button:hover {
background: #764ba2;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
#result {
margin-top: 30px;
padding: 20px;
border-radius: 5px;
display: none;
}
#result.success {
background: #d4edda;
color: #155724;
display: block;
}
#result.error {
background: #f8d7da;
color: #721c24;
display: block;
}
#loading {
display: none;
color: #667eea;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>🤖 Discord Commands Registration</h1>
<p>Click the button below to register all Discord slash commands (/verify, /set-realm, /profile, /unlink, /verify-role)</p>
<button id="registerBtn" onclick="registerCommands()">Register Commands</button>
<div id="loading"> Registering... please wait...</div>
<div id="result"></div>
</div>
<script>
async function registerCommands() {
const btn = document.getElementById('registerBtn');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
btn.disabled = true;
loading.style.display = 'block';
result.style.display = 'none';
try {
const response = await fetch('/register-commands', {
method: 'POST',
headers: {
'Authorization': 'Bearer aethex-link',
'Content-Type': 'application/json'
}
});
const data = await response.json();
loading.style.display = 'none';
result.style.display = 'block';
if (response.ok && data.success) {
result.className = 'success';
result.innerHTML = \`
<h3> Success!</h3>
<p>Registered \${data.count} commands</p>
\${data.skipped ? \`<p>(\${data.skipped} commands already existed)</p>\` : ''}
<p>You can now use the following commands in Discord:</p>
<ul>
<li>/verify - Link your account</li>
<li>/set-realm - Choose your realm</li>
<li>/profile - View your profile</li>
<li>/unlink - Disconnect account</li>
<li>/verify-role - Check your roles</li>
</ul>
\`;
} else {
result.className = 'error';
result.innerHTML = \`
<h3> Error</h3>
<p>\${data.error || 'Failed to register commands'}</p>
\`;
}
} catch (error) {
loading.style.display = 'none';
result.style.display = 'block';
result.className = 'error';
result.innerHTML = \`
<h3> Error</h3>
<p>\${error.message}</p>
\`;
} finally {
btn.disabled = false;
}
}
</script>
</body>
</html>
`);
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(`<EFBFBD><EFBFBD><EFBFBD><EFBFBD> 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;

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -0,0 +1,10 @@
TYPE=bot
MAIN=bot.js
NAME=AeThex
AVATAR=https://docs.aethex.tech/~gitbook/image?url=https%3A%2F%2F1143808467-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Forganizations%252FDhUg3jal6kdpG645FzIl%252Fsites%252Fsite_HeOmR%252Flogo%252FqxDYz8Oj2SnwUTa8t3UB%252FAeThex%2520Origin%2520logo.png%3Falt%3Dmedia%26token%3D200e8ea2-0129-4cbe-b516-4a53f60c512b&width=512&dpr=1&quality=100&sign=6c7576ce&sv=2
RAM=100
AUTORESTART=true
APT=tool, education, gamedev
START=npm install
BUILD=npm run build
VLAN=true

View file

@ -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,
);
}
}
},
};

View file

@ -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);
}
}
},
};

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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();

View file

@ -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<boolean>} - 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<string>} - 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,
};

View file

@ -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

View file

@ -0,0 +1,211 @@
# AeThex Discord Bot - Spaceship Deployment Guide
## 📋 Prerequisites
- Spaceship hosting account with Node.js support
- Discord bot credentials (already in your environment variables)
- Supabase project credentials
- Git access to your repository
## 🚀 Deployment Steps
### Step 1: Prepare the Bot Directory
Ensure all bot files are committed:
```
code/discord-bot/
├── bot.js
├── package.json
├── .env.example
├── Dockerfile
└── commands/
├── verify.js
├── set-realm.js
├── profile.js
├── unlink.js
└── verify-role.js
```
### Step 2: Create Node.js App on Spaceship
1. Log in to your Spaceship hosting dashboard
2. Click "Create New Application"
3. Select **Node.js** as the runtime
4. Name it: `aethex-discord-bot`
5. Select your repository and branch
### Step 3: Configure Environment Variables
In Spaceship Application Settings → Environment Variables, add:
```
DISCORD_BOT_TOKEN=<your_bot_token_from_discord_developer_portal>
DISCORD_CLIENT_ID=<your_client_id>
DISCORD_PUBLIC_KEY=<your_public_key>
SUPABASE_URL=<your_supabase_url>
SUPABASE_SERVICE_ROLE=<your_service_role_key>
BOT_PORT=3000
NODE_ENV=production
```
**Note:** Get these values from:
- Discord Developer Portal: Applications → Your Bot → Token & General Information
- Supabase Dashboard: Project Settings → API
### Step 4: Configure Build & Run Settings
In Spaceship Application Settings:
**Build Command:**
```bash
npm install
```
**Start Command:**
```bash
npm start
```
**Root Directory:**
```
code/discord-bot
```
### Step 5: Deploy
1. Click "Deploy" in Spaceship dashboard
2. Monitor logs for:
```
✅ Bot logged in as <BOT_NAME>#<ID>
📡 Listening in X server(s)
✅ Successfully registered X slash commands.
```
### Step 6: Verify Bot is Online
Once deployed:
1. Go to your Discord server
2. Type `/verify` - the command autocomplete should appear
3. Bot should be online with status "Listening to /verify to link your AeThex account"
## 📡 Discord Bot Endpoints
The bot will be accessible at:
```
https://<your-spaceship-domain>/
```
The bot uses Discord's WebSocket connection (not HTTP), so it doesn't need to expose HTTP endpoints. It listens to Discord events via `client.login(DISCORD_BOT_TOKEN)`.
## 🔌 API Integration
Frontend calls to link Discord accounts:
- **Endpoint:** `POST /api/discord/link`
- **Body:** `{ verification_code, user_id }`
- **Response:** `{ success: true, message: "..." }`
Discord Verify page (`/discord-verify?code=XXX`) will automatically:
1. Call `/api/discord/link` with the verification code
2. Link the Discord ID to the AeThex user account
3. Redirect to dashboard on success
## 🛠️ Debugging
### Check bot logs on Spaceship:
- Application → Logs
- Filter for "bot.js" or "error"
### Common issues:
**"Discord bot not responding to commands"**
- Check: `DISCORD_BOT_TOKEN` is correct
- Check: Bot is added to the Discord server with "applications.commands" scope
- Check: Spaceship logs show "✅ Logged in"
**"Supabase verification fails"**
- Check: `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE` are correct
- Check: `discord_links` and `discord_verifications` tables exist
- Run migration: `code/supabase/migrations/20250107_add_discord_integration.sql`
**"Slash commands not appearing in Discord"**
- Check: Logs show "✅ Successfully registered X slash commands"
- Discord may need 1-2 minutes to sync commands
- Try typing `/` in Discord to force refresh
- Check: Bot has "applications.commands" permission in server
## 📊 Monitoring
### Key metrics to monitor:
- Bot uptime (should be 24/7)
- Command usage (in Supabase)
- Verification code usage (in Supabase)
- Discord role sync success rate
### View in Admin Dashboard:
- AeThex Admin Panel → Discord Management tab
- Shows:
- Bot status
- Servers connected
- Linked accounts count
- Role mapping status
## 🔄 Updating the Bot
1. Make code changes locally
2. Test with `npm start`
3. Commit and push to your branch
4. Spaceship will auto-deploy on push
5. Monitor logs to ensure deployment succeeds
## 🆘 Support
For issues:
1. Check Spaceship logs
2. Review `/api/discord/link` endpoint response
3. Verify all environment variables are set correctly
4. Ensure Supabase tables exist and have correct schema
## 📝 Database Setup
Run this migration on your AeThex Supabase:
```sql
-- From code/supabase/migrations/20250107_add_discord_integration.sql
-- This creates:
-- - discord_links (links Discord ID to AeThex user)
-- - discord_verifications (temporary verification codes)
-- - discord_role_mappings (realm → Discord role mapping)
-- - discord_user_roles (tracking assigned roles)
```
## 🎉 You're All Set!
Once deployed, users can:
1. Click "Link Discord" in their profile settings
2. Type `/verify` in Discord
3. Click the verification link
4. Their Discord account is linked to their AeThex account
5. They can use `/set-realm`, `/profile`, `/unlink`, and `/verify-role` commands
---
**Deployment Date:** `<date>`
**Bot Status:** `<status>`
**Last Updated:** `<timestamp>`

View file

@ -0,0 +1,22 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install --production
# Copy bot source
COPY . .
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
# Start bot
CMD ["npm", "start"]

View file

@ -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(`
<!DOCTYPE html>
<html>
<head>
<title>Register Discord Commands</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
text-align: center;
max-width: 500px;
}
h1 {
color: #333;
margin-bottom: 20px;
}
p {
color: #666;
margin-bottom: 30px;
}
button {
background: #667eea;
color: white;
border: none;
padding: 12px 30px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: background 0.3s;
}
button:hover {
background: #764ba2;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
#result {
margin-top: 30px;
padding: 20px;
border-radius: 5px;
display: none;
}
#result.success {
background: #d4edda;
color: #155724;
display: block;
}
#result.error {
background: #f8d7da;
color: #721c24;
display: block;
}
#loading {
display: none;
color: #667eea;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>🤖 Discord Commands Registration</h1>
<p>Click the button below to register all Discord slash commands (/verify, /set-realm, /profile, /unlink, /verify-role)</p>
<button id="registerBtn" onclick="registerCommands()">Register Commands</button>
<div id="loading"> Registering... please wait...</div>
<div id="result"></div>
</div>
<script>
async function registerCommands() {
const btn = document.getElementById('registerBtn');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
btn.disabled = true;
loading.style.display = 'block';
result.style.display = 'none';
try {
const response = await fetch('/register-commands', {
method: 'POST',
headers: {
'Authorization': 'Bearer aethex-link',
'Content-Type': 'application/json'
}
});
const data = await response.json();
loading.style.display = 'none';
result.style.display = 'block';
if (response.ok && data.success) {
result.className = 'success';
result.innerHTML = \`
<h3> Success!</h3>
<p>Registered \${data.count} commands</p>
\${data.skipped ? \`<p>(\${data.skipped} commands already existed)</p>\` : ''}
<p>You can now use the following commands in Discord:</p>
<ul>
<li>/verify - Link your account</li>
<li>/set-realm - Choose your realm</li>
<li>/profile - View your profile</li>
<li>/unlink - Disconnect account</li>
<li>/verify-role - Check your roles</li>
</ul>
\`;
} else {
result.className = 'error';
result.innerHTML = \`
<h3> Error</h3>
<p>\${data.error || 'Failed to register commands'}</p>
\`;
}
} catch (error) {
loading.style.display = 'none';
result.style.display = 'block';
result.className = 'error';
result.innerHTML = \`
<h3> Error</h3>
<p>\${error.message}</p>
\`;
} finally {
btn.disabled = false;
}
}
</script>
</body>
</html>
`);
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(`<EFBFBD><EFBFBD><EFBFBD><EFBFBD> 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;

View file

@ -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 });
},
};

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -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] });
}
},
};

View file

@ -0,0 +1,10 @@
TYPE=bot
MAIN=bot.js
NAME=AeThex
AVATAR=https://docs.aethex.tech/~gitbook/image?url=https%3A%2F%2F1143808467-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Forganizations%252FDhUg3jal6kdpG645FzIl%252Fsites%252Fsite_HeOmR%252Flogo%252FqxDYz8Oj2SnwUTa8t3UB%252FAeThex%2520Origin%2520logo.png%3Falt%3Dmedia%26token%3D200e8ea2-0129-4cbe-b516-4a53f60c512b&width=512&dpr=1&quality=100&sign=6c7576ce&sv=2
RAM=100
AUTORESTART=true
APT=tool, education, gamedev
START=npm install
BUILD=npm run build
VLAN=true

View file

@ -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);
},
};

View file

@ -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 };

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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();

View file

@ -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<boolean>} - 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<string>} - 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,
};

Binary file not shown.

Binary file not shown.