Restored to '00d37533c9543238868c8d5732ffe0f07fdb3059'

Replit-Restored-To: 00d37533c9
This commit is contained in:
sirpiglr 2025-12-07 23:24:45 +00:00
parent 77ae722b73
commit 54ba9b2e1f
62 changed files with 11305 additions and 0 deletions

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

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 || 8080;
const ADMIN_TOKEN = process.env.DISCORD_ADMIN_TOKEN || "aethex-bot-admin";
// Helper to check admin authentication
const checkAdminAuth = (req) => {
const authHeader = req.headers.authorization;
return authHeader === `Bearer ${ADMIN_TOKEN}`;
};
http
.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Content-Type", "application/json");
if (req.method === "OPTIONS") {
res.writeHead(200);
res.end();
return;
}
if (req.url === "/health") {
res.writeHead(200);
res.end(
JSON.stringify({
status: "online",
guilds: client.guilds.cache.size,
commands: client.commands.size,
uptime: Math.floor(process.uptime()),
timestamp: new Date().toISOString(),
}),
);
return;
}
// GET /bot-status - Comprehensive bot status for management panel (requires auth)
if (req.url === "/bot-status") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
const channelId = getFeedChannelId();
const guilds = client.guilds.cache.map((guild) => ({
id: guild.id,
name: guild.name,
memberCount: guild.memberCount,
icon: guild.iconURL(),
}));
res.writeHead(200);
res.end(
JSON.stringify({
status: client.isReady() ? "online" : "offline",
bot: {
tag: client.user?.tag || "Not logged in",
id: client.user?.id,
avatar: client.user?.displayAvatarURL(),
},
guilds: guilds,
guildCount: client.guilds.cache.size,
commands: Array.from(client.commands.keys()),
commandCount: client.commands.size,
uptime: Math.floor(process.uptime()),
feedBridge: {
enabled: !!channelId,
channelId: channelId,
},
timestamp: new Date().toISOString(),
}),
);
return;
}
// GET /linked-users - Get all Discord-linked users (requires auth, sanitizes PII)
if (req.url === "/linked-users") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
(async () => {
try {
const { data: links, error } = await supabase
.from("discord_links")
.select("discord_id, user_id, primary_arm, created_at")
.order("created_at", { ascending: false })
.limit(50);
if (error) throw error;
const enrichedLinks = await Promise.all(
(links || []).map(async (link) => {
const { data: profile } = await supabase
.from("user_profiles")
.select("username, avatar_url")
.eq("id", link.user_id)
.single();
return {
discord_id: link.discord_id.slice(0, 6) + "***",
user_id: link.user_id.slice(0, 8) + "...",
primary_arm: link.primary_arm,
created_at: link.created_at,
profile: profile ? {
username: profile.username,
avatar_url: profile.avatar_url,
} : null,
};
})
);
res.writeHead(200);
res.end(JSON.stringify({ success: true, links: enrichedLinks, count: enrichedLinks.length }));
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ success: false, error: error.message }));
}
})();
return;
}
// GET /command-stats - Get command usage statistics (requires auth)
if (req.url === "/command-stats") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
(async () => {
try {
const stats = {
commands: COMMANDS_TO_REGISTER.map((cmd) => ({
name: cmd.name,
description: cmd.description,
options: cmd.options?.length || 0,
})),
totalCommands: COMMANDS_TO_REGISTER.length,
};
res.writeHead(200);
res.end(JSON.stringify({ success: true, stats }));
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ success: false, error: error.message }));
}
})();
return;
}
// GET /feed-stats - Get feed bridge statistics (requires auth)
if (req.url === "/feed-stats") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
(async () => {
try {
const { count: totalPosts } = await supabase
.from("community_posts")
.select("*", { count: "exact", head: true });
const { count: discordPosts } = await supabase
.from("community_posts")
.select("*", { count: "exact", head: true })
.eq("source", "discord");
const { count: websitePosts } = await supabase
.from("community_posts")
.select("*", { count: "exact", head: true })
.or("source.is.null,source.neq.discord");
const { data: recentPosts } = await supabase
.from("community_posts")
.select("id, content, source, created_at")
.order("created_at", { ascending: false })
.limit(10);
res.writeHead(200);
res.end(
JSON.stringify({
success: true,
stats: {
totalPosts: totalPosts || 0,
discordPosts: discordPosts || 0,
websitePosts: websitePosts || 0,
recentPosts: (recentPosts || []).map(p => ({
id: p.id,
content: p.content?.slice(0, 100) + (p.content?.length > 100 ? "..." : ""),
source: p.source,
created_at: p.created_at,
})),
},
})
);
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ success: false, error: error.message }));
}
})();
return;
}
// POST /send-to-discord - Send a post from AeThex to Discord channel
if (req.url === "/send-to-discord" && req.method === "POST") {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", async () => {
try {
// Simple auth check
const authHeader = req.headers.authorization;
const expectedToken = process.env.DISCORD_BRIDGE_TOKEN || "aethex-bridge";
if (authHeader !== `Bearer ${expectedToken}`) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized" }));
return;
}
const post = JSON.parse(body);
console.log("[API] Received post to send to Discord:", post.id);
const result = await sendPostToDiscord(post, post.author);
res.writeHead(result.success ? 200 : 500);
res.end(JSON.stringify(result));
} catch (error) {
console.error("[API] Error processing send-to-discord:", error);
res.writeHead(500);
res.end(JSON.stringify({ error: error.message }));
}
});
return;
}
// GET /bridge-status - Check if bridge is configured
if (req.url === "/bridge-status") {
const channelId = getFeedChannelId();
res.writeHead(200);
res.end(
JSON.stringify({
enabled: !!channelId,
channelId: channelId,
botReady: client.isReady(),
}),
);
return;
}
if (req.url === "/register-commands") {
if (req.method === "GET") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
return;
}
// Show HTML form with button
res.writeHead(200, { "Content-Type": "text/html" });
res.end(`
<!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.

Binary file not shown.