Add Discord bot functionality for AeThex platform integration
Adds two Node.js Discord bots for the AeThex platform, enabling features such as account linking, role management, profile viewing, community posts, and more, with extensive Supabase integration. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e72fc1b7-94bd-4d6c-801f-cbac2fae245c Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: f8fda02a-6ff3-4bdf-87d4-fdbef7f9a2ce Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
3b82e95e95
commit
02e50ed478
41 changed files with 7328 additions and 1 deletions
5
.replit
5
.replit
|
|
@ -9,4 +9,7 @@ channel = "stable-23_05"
|
|||
[deployment]
|
||||
run = ["python", "main.py"]
|
||||
deploymentTarget = "gce"
|
||||
ignorePorts = true
|
||||
ignorePorts = true
|
||||
|
||||
[agent]
|
||||
expertMode = true
|
||||
|
|
|
|||
23
attached_assets/bot1/discord-bot/.env.example
Normal file
23
attached_assets/bot1/discord-bot/.env.example
Normal 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
|
||||
211
attached_assets/bot1/discord-bot/DEPLOYMENT_GUIDE.md
Normal file
211
attached_assets/bot1/discord-bot/DEPLOYMENT_GUIDE.md
Normal 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>`
|
||||
22
attached_assets/bot1/discord-bot/Dockerfile
Normal file
22
attached_assets/bot1/discord-bot/Dockerfile
Normal 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"]
|
||||
522
attached_assets/bot1/discord-bot/bot.js
Normal file
522
attached_assets/bot1/discord-bot/bot.js
Normal 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;
|
||||
93
attached_assets/bot1/discord-bot/commands/profile.js
Normal file
93
attached_assets/bot1/discord-bot/commands/profile.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
72
attached_assets/bot1/discord-bot/commands/refresh-roles.js
Normal file
72
attached_assets/bot1/discord-bot/commands/refresh-roles.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
139
attached_assets/bot1/discord-bot/commands/set-realm.js
Normal file
139
attached_assets/bot1/discord-bot/commands/set-realm.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
75
attached_assets/bot1/discord-bot/commands/unlink.js
Normal file
75
attached_assets/bot1/discord-bot/commands/unlink.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
97
attached_assets/bot1/discord-bot/commands/verify-role.js
Normal file
97
attached_assets/bot1/discord-bot/commands/verify-role.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
84
attached_assets/bot1/discord-bot/commands/verify.js
Normal file
84
attached_assets/bot1/discord-bot/commands/verify.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
10
attached_assets/bot1/discord-bot/discloud.config
Normal file
10
attached_assets/bot1/discord-bot/discloud.config
Normal 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
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
320
attached_assets/bot1/discord-bot/events/messageCreate.js
Normal file
320
attached_assets/bot1/discord-bot/events/messageCreate.js
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
1157
attached_assets/bot1/discord-bot/package-lock.json
generated
Normal file
1157
attached_assets/bot1/discord-bot/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
34
attached_assets/bot1/discord-bot/package.json
Normal file
34
attached_assets/bot1/discord-bot/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
110
attached_assets/bot1/discord-bot/scripts/register-commands.js
Normal file
110
attached_assets/bot1/discord-bot/scripts/register-commands.js
Normal 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();
|
||||
137
attached_assets/bot1/discord-bot/utils/roleManager.js
Normal file
137
attached_assets/bot1/discord-bot/utils/roleManager.js
Normal 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,
|
||||
};
|
||||
23
attached_assets/bot2/discord-bot/.env.example
Normal file
23
attached_assets/bot2/discord-bot/.env.example
Normal 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
|
||||
211
attached_assets/bot2/discord-bot/DEPLOYMENT_GUIDE.md
Normal file
211
attached_assets/bot2/discord-bot/DEPLOYMENT_GUIDE.md
Normal 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>`
|
||||
22
attached_assets/bot2/discord-bot/Dockerfile
Normal file
22
attached_assets/bot2/discord-bot/Dockerfile
Normal 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"]
|
||||
803
attached_assets/bot2/discord-bot/bot.js
Normal file
803
attached_assets/bot2/discord-bot/bot.js
Normal 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;
|
||||
55
attached_assets/bot2/discord-bot/commands/help.js
Normal file
55
attached_assets/bot2/discord-bot/commands/help.js
Normal 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 });
|
||||
},
|
||||
};
|
||||
155
attached_assets/bot2/discord-bot/commands/leaderboard.js
Normal file
155
attached_assets/bot2/discord-bot/commands/leaderboard.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
144
attached_assets/bot2/discord-bot/commands/post.js
Normal file
144
attached_assets/bot2/discord-bot/commands/post.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
93
attached_assets/bot2/discord-bot/commands/profile.js
Normal file
93
attached_assets/bot2/discord-bot/commands/profile.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
72
attached_assets/bot2/discord-bot/commands/refresh-roles.js
Normal file
72
attached_assets/bot2/discord-bot/commands/refresh-roles.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
139
attached_assets/bot2/discord-bot/commands/set-realm.js
Normal file
139
attached_assets/bot2/discord-bot/commands/set-realm.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
140
attached_assets/bot2/discord-bot/commands/stats.js
Normal file
140
attached_assets/bot2/discord-bot/commands/stats.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
75
attached_assets/bot2/discord-bot/commands/unlink.js
Normal file
75
attached_assets/bot2/discord-bot/commands/unlink.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
97
attached_assets/bot2/discord-bot/commands/verify-role.js
Normal file
97
attached_assets/bot2/discord-bot/commands/verify-role.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
85
attached_assets/bot2/discord-bot/commands/verify.js
Normal file
85
attached_assets/bot2/discord-bot/commands/verify.js
Normal 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] });
|
||||
}
|
||||
},
|
||||
};
|
||||
10
attached_assets/bot2/discord-bot/discloud.config
Normal file
10
attached_assets/bot2/discord-bot/discloud.config
Normal 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
|
||||
180
attached_assets/bot2/discord-bot/events/messageCreate.js
Normal file
180
attached_assets/bot2/discord-bot/events/messageCreate.js
Normal 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);
|
||||
},
|
||||
};
|
||||
239
attached_assets/bot2/discord-bot/listeners/feedSync.js
Normal file
239
attached_assets/bot2/discord-bot/listeners/feedSync.js
Normal 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 };
|
||||
1157
attached_assets/bot2/discord-bot/package-lock.json
generated
Normal file
1157
attached_assets/bot2/discord-bot/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
34
attached_assets/bot2/discord-bot/package.json
Normal file
34
attached_assets/bot2/discord-bot/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
110
attached_assets/bot2/discord-bot/scripts/register-commands.js
Normal file
110
attached_assets/bot2/discord-bot/scripts/register-commands.js
Normal 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();
|
||||
137
attached_assets/bot2/discord-bot/utils/roleManager.js
Normal file
137
attached_assets/bot2/discord-bot/utils/roleManager.js
Normal 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,
|
||||
};
|
||||
BIN
attached_assets/discord-bot_(1)_1765057157676.zip
Normal file
BIN
attached_assets/discord-bot_(1)_1765057157676.zip
Normal file
Binary file not shown.
BIN
attached_assets/discord-bot_1765057157677.zip
Normal file
BIN
attached_assets/discord-bot_1765057157677.zip
Normal file
Binary file not shown.
Loading…
Reference in a new issue