Add missing commands and functionality to the bot

Restores and registers previously missing commands such as /verify, /profile, /leaderboard, and others, alongside their associated functionality and the role management utilities.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e72fc1b7-94bd-4d6c-801f-cbac2fae245c
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 9f9fe241-9650-4ed0-9631-2a4d2267f526
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/e72fc1b7-94bd-4d6c-801f-cbac2fae245c/MSxeu36
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-07 22:39:17 +00:00
parent 42c2ba799f
commit ba613d2a7c
12 changed files with 1274 additions and 1 deletions

View file

@ -0,0 +1,55 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("help")
.setDescription("View all AeThex bot commands and features"),
async execute(interaction) {
const embed = new EmbedBuilder()
.setColor(0x7289da)
.setTitle("🤖 AeThex Bot Commands")
.setDescription("Here are all the commands you can use with the AeThex Discord bot.")
.addFields(
{
name: "🔗 Account Linking",
value: [
"`/verify` - Link your Discord account to AeThex",
"`/unlink` - Disconnect your Discord from AeThex",
"`/profile` - View your linked AeThex profile",
].join("\n"),
},
{
name: "⚔️ Realm Management",
value: [
"`/set-realm` - Choose your primary realm (Labs, GameForge, Corp, Foundation, Dev-Link)",
"`/verify-role` - Check your assigned Discord roles",
].join("\n"),
},
{
name: "📊 Community",
value: [
"`/stats` - View your AeThex statistics and activity",
"`/leaderboard` - See the top contributors",
"`/post` - Create a post in the AeThex community feed",
].join("\n"),
},
{
name: " Information",
value: "`/help` - Show this help message",
},
)
.addFields({
name: "🔗 Quick Links",
value: [
"[AeThex Platform](https://aethex.dev)",
"[Creator Directory](https://aethex.dev/creators)",
"[Community Feed](https://aethex.dev/community/feed)",
].join(" | "),
})
.setFooter({ text: "AeThex | Build. Create. Connect." })
.setTimestamp();
await interaction.reply({ embeds: [embed], ephemeral: true });
},
};

View file

@ -0,0 +1,155 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("leaderboard")
.setDescription("View the top AeThex contributors")
.addStringOption((option) =>
option
.setName("category")
.setDescription("Leaderboard category")
.setRequired(false)
.addChoices(
{ name: "🔥 Most Active (Posts)", value: "posts" },
{ name: "❤️ Most Liked", value: "likes" },
{ name: "🎨 Top Creators", value: "creators" }
)
),
async execute(interaction, supabase) {
await interaction.deferReply();
try {
const category = interaction.options.getString("category") || "posts";
let leaderboardData = [];
let title = "";
let emoji = "";
if (category === "posts") {
title = "Most Active Posters";
emoji = "🔥";
const { data: posts } = await supabase
.from("community_posts")
.select("user_id")
.not("user_id", "is", null);
const postCounts = {};
posts?.forEach((post) => {
postCounts[post.user_id] = (postCounts[post.user_id] || 0) + 1;
});
const sortedUsers = Object.entries(postCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10);
for (const [userId, count] of sortedUsers) {
const { data: profile } = await supabase
.from("user_profiles")
.select("username, full_name, avatar_url")
.eq("id", userId)
.single();
if (profile) {
leaderboardData.push({
name: profile.full_name || profile.username || "Anonymous",
value: `${count} posts`,
username: profile.username,
});
}
}
} else if (category === "likes") {
title = "Most Liked Users";
emoji = "❤️";
const { data: posts } = await supabase
.from("community_posts")
.select("user_id, likes_count")
.not("user_id", "is", null)
.order("likes_count", { ascending: false });
const likeCounts = {};
posts?.forEach((post) => {
likeCounts[post.user_id] =
(likeCounts[post.user_id] || 0) + (post.likes_count || 0);
});
const sortedUsers = Object.entries(likeCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10);
for (const [userId, count] of sortedUsers) {
const { data: profile } = await supabase
.from("user_profiles")
.select("username, full_name, avatar_url")
.eq("id", userId)
.single();
if (profile) {
leaderboardData.push({
name: profile.full_name || profile.username || "Anonymous",
value: `${count} likes received`,
username: profile.username,
});
}
}
} else if (category === "creators") {
title = "Top Creators";
emoji = "🎨";
const { data: creators } = await supabase
.from("aethex_creators")
.select("user_id, total_projects, verified, featured")
.order("total_projects", { ascending: false })
.limit(10);
for (const creator of creators || []) {
const { data: profile } = await supabase
.from("user_profiles")
.select("username, full_name, avatar_url")
.eq("id", creator.user_id)
.single();
if (profile) {
const badges = [];
if (creator.verified) badges.push("✅");
if (creator.featured) badges.push("⭐");
leaderboardData.push({
name: profile.full_name || profile.username || "Anonymous",
value: `${creator.total_projects || 0} projects ${badges.join(" ")}`,
username: profile.username,
});
}
}
}
const embed = new EmbedBuilder()
.setColor(0x7289da)
.setTitle(`${emoji} ${title}`)
.setDescription(
leaderboardData.length > 0
? leaderboardData
.map(
(user, index) =>
`**${index + 1}.** ${user.name} - ${user.value}`
)
.join("\n")
: "No data available yet. Be the first to contribute!"
)
.setFooter({ text: "AeThex Leaderboard | Updated in real-time" })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error("Leaderboard command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("❌ Error")
.setDescription("Failed to fetch leaderboard. Please try again.");
await interaction.editReply({ embeds: [embed] });
}
},
};

144
aethex-bot/commands/post.js Normal file
View file

@ -0,0 +1,144 @@
const {
SlashCommandBuilder,
EmbedBuilder,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ActionRowBuilder,
} = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("post")
.setDescription("Create a post in the AeThex community feed")
.addStringOption((option) =>
option
.setName("content")
.setDescription("Your post content")
.setRequired(true)
.setMaxLength(500)
)
.addStringOption((option) =>
option
.setName("category")
.setDescription("Post category")
.setRequired(false)
.addChoices(
{ name: "💬 General", value: "general" },
{ name: "🚀 Project Update", value: "project_update" },
{ name: "❓ Question", value: "question" },
{ name: "💡 Idea", value: "idea" },
{ name: "🎉 Announcement", value: "announcement" }
)
)
.addAttachmentOption((option) =>
option
.setName("image")
.setDescription("Attach an image to your post")
.setRequired(false)
),
async execute(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
try {
const { data: link } = await supabase
.from("discord_links")
.select("user_id, primary_arm")
.eq("discord_id", interaction.user.id)
.single();
if (!link) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("❌ Not Linked")
.setDescription(
"You must link your Discord account to AeThex first.\nUse `/verify` to get started."
);
return await interaction.editReply({ embeds: [embed] });
}
const { data: profile } = await supabase
.from("user_profiles")
.select("username, full_name, avatar_url")
.eq("id", link.user_id)
.single();
const content = interaction.options.getString("content");
const category = interaction.options.getString("category") || "general";
const attachment = interaction.options.getAttachment("image");
let imageUrl = null;
if (attachment && attachment.contentType?.startsWith("image/")) {
imageUrl = attachment.url;
}
const categoryLabels = {
general: "General",
project_update: "Project Update",
question: "Question",
idea: "Idea",
announcement: "Announcement",
};
const { data: post, error } = await supabase
.from("community_posts")
.insert({
user_id: link.user_id,
content: content,
category: category,
arm_affiliation: link.primary_arm || "general",
image_url: imageUrl,
source: "discord",
discord_message_id: interaction.id,
discord_author_id: interaction.user.id,
discord_author_name: interaction.user.username,
discord_author_avatar: interaction.user.displayAvatarURL(),
})
.select()
.single();
if (error) throw error;
const successEmbed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle("✅ Post Created!")
.setDescription(content.length > 100 ? content.slice(0, 100) + "..." : content)
.addFields(
{
name: "📁 Category",
value: categoryLabels[category],
inline: true,
},
{
name: "⚔️ Realm",
value: link.primary_arm || "general",
inline: true,
}
);
if (imageUrl) {
successEmbed.setImage(imageUrl);
}
successEmbed
.addFields({
name: "🔗 View Post",
value: `[Open in AeThex](https://aethex.dev/community/feed)`,
})
.setFooter({ text: "Your post is now live on AeThex!" })
.setTimestamp();
await interaction.editReply({ embeds: [successEmbed] });
} catch (error) {
console.error("Post command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("❌ Error")
.setDescription("Failed to create post. Please try again.");
await interaction.editReply({ embeds: [embed] });
}
},
};

View file

@ -0,0 +1,93 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("profile")
.setDescription("View your AeThex profile in Discord"),
async execute(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
try {
const { data: link } = await supabase
.from("discord_links")
.select("user_id, primary_arm")
.eq("discord_id", interaction.user.id)
.single();
if (!link) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("❌ Not Linked")
.setDescription(
"You must link your Discord account to AeThex first.\nUse `/verify` to get started.",
);
return await interaction.editReply({ embeds: [embed] });
}
const { data: profile } = await supabase
.from("user_profiles")
.select("*")
.eq("id", link.user_id)
.single();
if (!profile) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("❌ Profile Not Found")
.setDescription("Your AeThex profile could not be found.");
return await interaction.editReply({ embeds: [embed] });
}
const armEmojis = {
labs: "🧪",
gameforge: "🎮",
corp: "💼",
foundation: "🤝",
devlink: "💻",
};
const embed = new EmbedBuilder()
.setColor(0x7289da)
.setTitle(`${profile.full_name || "AeThex User"}'s Profile`)
.setThumbnail(
profile.avatar_url || "https://aethex.dev/placeholder.svg",
)
.addFields(
{
name: "👤 Username",
value: profile.username || "N/A",
inline: true,
},
{
name: `${armEmojis[link.primary_arm] || "⚔️"} Primary Realm`,
value: link.primary_arm || "Not set",
inline: true,
},
{
name: "📊 Role",
value: profile.user_type || "community_member",
inline: true,
},
{ name: "📝 Bio", value: profile.bio || "No bio set", inline: false },
)
.addFields({
name: "🔗 Links",
value: `[Visit Full Profile](https://aethex.dev/creators/${profile.username})`,
})
.setFooter({ text: "AeThex | Your Web3 Creator Hub" });
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error("Profile command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("❌ Error")
.setDescription("Failed to fetch profile. Please try again.");
await interaction.editReply({ embeds: [embed] });
}
},
};

View file

@ -0,0 +1,72 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
const { assignRoleByArm, getUserArm } = require("../utils/roleManager");
module.exports = {
data: new SlashCommandBuilder()
.setName("refresh-roles")
.setDescription(
"Refresh your Discord roles based on your current AeThex settings",
),
async execute(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
try {
// Check if user is linked
const { data: link } = await supabase
.from("discord_links")
.select("primary_arm")
.eq("discord_id", interaction.user.id)
.maybeSingle();
if (!link) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("❌ Not Linked")
.setDescription(
"You must link your Discord account to AeThex first.\nUse `/verify` to get started.",
);
return await interaction.editReply({ embeds: [embed] });
}
if (!link.primary_arm) {
const embed = new EmbedBuilder()
.setColor(0xffaa00)
.setTitle("⚠️ No Realm Set")
.setDescription(
"You haven't set your primary realm yet.\nUse `/set-realm` to choose one.",
);
return await interaction.editReply({ embeds: [embed] });
}
// Assign role based on current primary arm
const roleAssigned = await assignRoleByArm(
interaction.guild,
interaction.user.id,
link.primary_arm,
supabase,
);
const embed = new EmbedBuilder()
.setColor(roleAssigned ? 0x00ff00 : 0xffaa00)
.setTitle("✅ Roles Refreshed")
.setDescription(
roleAssigned
? `Your Discord roles have been synced with your AeThex account.\n\nPrimary Realm: **${link.primary_arm}**`
: `Your roles could not be automatically assigned.\n\nPrimary Realm: **${link.primary_arm}**\n\n⚠️ Please contact an admin to set up the role mapping for this server.`,
);
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error("Refresh-roles command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("❌ Error")
.setDescription("Failed to refresh roles. Please try again.");
await interaction.editReply({ embeds: [embed] });
}
},
};

View file

@ -0,0 +1,139 @@
const {
SlashCommandBuilder,
EmbedBuilder,
StringSelectMenuBuilder,
ActionRowBuilder,
} = require("discord.js");
const { assignRoleByArm } = require("../utils/roleManager");
const REALMS = [
{ value: "labs", label: "🧪 Labs", description: "Research & Development" },
{
value: "gameforge",
label: "🎮 GameForge",
description: "Game Development",
},
{ value: "corp", label: "💼 Corp", description: "Enterprise Solutions" },
{
value: "foundation",
label: "🤝 Foundation",
description: "Community & Education",
},
{
value: "devlink",
label: "💻 Dev-Link",
description: "Professional Networking",
},
];
module.exports = {
data: new SlashCommandBuilder()
.setName("set-realm")
.setDescription("Set your primary AeThex realm/arm"),
async execute(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
try {
const { data: link } = await supabase
.from("discord_links")
.select("user_id, primary_arm")
.eq("discord_id", interaction.user.id)
.single();
if (!link) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("❌ Not Linked")
.setDescription(
"You must link your Discord account to AeThex first.\nUse `/verify` to get started.",
);
return await interaction.editReply({ embeds: [embed] });
}
const select = new StringSelectMenuBuilder()
.setCustomId("select_realm")
.setPlaceholder("Choose your primary realm")
.addOptions(
REALMS.map((realm) => ({
label: realm.label,
description: realm.description,
value: realm.value,
default: realm.value === link.primary_arm,
})),
);
const row = new ActionRowBuilder().addComponents(select);
const embed = new EmbedBuilder()
.setColor(0x7289da)
.setTitle("⚔️ Choose Your Realm")
.setDescription(
"Select your primary AeThex realm. This determines your main Discord role.",
)
.addFields({
name: "Current Realm",
value: link.primary_arm || "Not set",
});
await interaction.editReply({ embeds: [embed], components: [row] });
const filter = (i) =>
i.user.id === interaction.user.id && i.customId === "select_realm";
const collector = interaction.channel.createMessageComponentCollector({
filter,
time: 60000,
});
collector.on("collect", async (i) => {
const selectedRealm = i.values[0];
await supabase
.from("discord_links")
.update({ primary_arm: selectedRealm })
.eq("discord_id", interaction.user.id);
const realm = REALMS.find((r) => r.value === selectedRealm);
// Assign Discord role based on selected realm
const roleAssigned = await assignRoleByArm(
interaction.guild,
interaction.user.id,
selectedRealm,
supabase,
);
const roleStatus = roleAssigned
? "✅ Discord role assigned!"
: "⚠️ No role mapping found for this realm in this server.";
const confirmEmbed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle("✅ Realm Set")
.setDescription(
`Your primary realm is now **${realm.label}**\n\n${roleStatus}`,
);
await i.update({ embeds: [confirmEmbed], components: [] });
});
collector.on("end", (collected) => {
if (collected.size === 0) {
interaction.editReply({
content: "Realm selection timed out.",
components: [],
});
}
});
} catch (error) {
console.error("Set-realm command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("❌ Error")
.setDescription("Failed to update realm. Please try again.");
await interaction.editReply({ embeds: [embed] });
}
},
};

View file

@ -0,0 +1,140 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("stats")
.setDescription("View your AeThex statistics and activity"),
async execute(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
try {
const { data: link } = await supabase
.from("discord_links")
.select("user_id, primary_arm, created_at")
.eq("discord_id", interaction.user.id)
.single();
if (!link) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("❌ Not Linked")
.setDescription(
"You must link your Discord account to AeThex first.\nUse `/verify` to get started."
);
return await interaction.editReply({ embeds: [embed] });
}
const { data: profile } = await supabase
.from("user_profiles")
.select("*")
.eq("id", link.user_id)
.single();
const { count: postCount } = await supabase
.from("community_posts")
.select("*", { count: "exact", head: true })
.eq("user_id", link.user_id);
const { count: likeCount } = await supabase
.from("community_likes")
.select("*", { count: "exact", head: true })
.eq("user_id", link.user_id);
const { count: commentCount } = await supabase
.from("community_comments")
.select("*", { count: "exact", head: true })
.eq("user_id", link.user_id);
const { data: creatorProfile } = await supabase
.from("aethex_creators")
.select("verified, featured, total_projects")
.eq("user_id", link.user_id)
.single();
const armEmojis = {
labs: "🧪",
gameforge: "🎮",
corp: "💼",
foundation: "🤝",
devlink: "💻",
};
const linkedDate = new Date(link.created_at);
const daysSinceLinked = Math.floor(
(Date.now() - linkedDate.getTime()) / (1000 * 60 * 60 * 24)
);
const embed = new EmbedBuilder()
.setColor(0x7289da)
.setTitle(`📊 ${profile?.full_name || interaction.user.username}'s Stats`)
.setThumbnail(profile?.avatar_url || interaction.user.displayAvatarURL())
.addFields(
{
name: `${armEmojis[link.primary_arm] || "⚔️"} Primary Realm`,
value: link.primary_arm || "Not set",
inline: true,
},
{
name: "👤 Account Type",
value: profile?.user_type || "community_member",
inline: true,
},
{
name: "📅 Days Linked",
value: `${daysSinceLinked} days`,
inline: true,
}
)
.addFields(
{
name: "📝 Posts",
value: `${postCount || 0}`,
inline: true,
},
{
name: "❤️ Likes Given",
value: `${likeCount || 0}`,
inline: true,
},
{
name: "💬 Comments",
value: `${commentCount || 0}`,
inline: true,
}
);
if (creatorProfile) {
embed.addFields({
name: "🎨 Creator Status",
value: [
creatorProfile.verified ? "✅ Verified Creator" : "⏳ Pending Verification",
creatorProfile.featured ? "⭐ Featured" : "",
`📁 ${creatorProfile.total_projects || 0} Projects`,
]
.filter(Boolean)
.join("\n"),
});
}
embed
.addFields({
name: "🔗 Full Profile",
value: `[View on AeThex](https://aethex.dev/creators/${profile?.username || link.user_id})`,
})
.setFooter({ text: "AeThex | Your Creative Hub" })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error("Stats command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("❌ Error")
.setDescription("Failed to fetch stats. Please try again.");
await interaction.editReply({ embeds: [embed] });
}
},
};

View file

@ -0,0 +1,75 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("unlink")
.setDescription("Unlink your Discord account from AeThex"),
async execute(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
try {
const { data: link } = await supabase
.from("discord_links")
.select("*")
.eq("discord_id", interaction.user.id)
.single();
if (!link) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle(" Not Linked")
.setDescription("Your Discord account is not linked to AeThex.");
return await interaction.editReply({ embeds: [embed] });
}
// Delete the link
await supabase
.from("discord_links")
.delete()
.eq("discord_id", interaction.user.id);
// Remove Discord roles from user
const guild = interaction.guild;
const member = await guild.members.fetch(interaction.user.id);
// Find and remove all AeThex-related roles
const rolesToRemove = member.roles.cache.filter(
(role) =>
role.name.includes("Labs") ||
role.name.includes("GameForge") ||
role.name.includes("Corp") ||
role.name.includes("Foundation") ||
role.name.includes("Dev-Link") ||
role.name.includes("Premium") ||
role.name.includes("Creator"),
);
for (const [, role] of rolesToRemove) {
try {
await member.roles.remove(role);
} catch (e) {
console.warn(`Could not remove role ${role.name}`);
}
}
const embed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle("✅ Account Unlinked")
.setDescription(
"Your Discord account has been unlinked from AeThex.\nAll associated roles have been removed.",
);
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error("Unlink command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("❌ Error")
.setDescription("Failed to unlink account. Please try again.");
await interaction.editReply({ embeds: [embed] });
}
},
};

View file

@ -0,0 +1,97 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("verify-role")
.setDescription("Check your AeThex-assigned Discord roles"),
async execute(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
try {
const { data: link } = await supabase
.from("discord_links")
.select("user_id, primary_arm")
.eq("discord_id", interaction.user.id)
.single();
if (!link) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("❌ Not Linked")
.setDescription(
"You must link your Discord account to AeThex first.\nUse `/verify` to get started.",
);
return await interaction.editReply({ embeds: [embed] });
}
const { data: profile } = await supabase
.from("user_profiles")
.select("user_type")
.eq("id", link.user_id)
.single();
const { data: mappings } = await supabase
.from("discord_role_mappings")
.select("discord_role")
.eq("arm", link.primary_arm)
.eq("user_type", profile?.user_type || "community_member");
const member = await interaction.guild.members.fetch(interaction.user.id);
const aethexRoles = member.roles.cache.filter(
(role) =>
role.name.includes("Labs") ||
role.name.includes("GameForge") ||
role.name.includes("Corp") ||
role.name.includes("Foundation") ||
role.name.includes("Dev-Link") ||
role.name.includes("Premium") ||
role.name.includes("Creator"),
);
const embed = new EmbedBuilder()
.setColor(0x7289da)
.setTitle("🔐 Your AeThex Roles")
.addFields(
{
name: "⚔️ Primary Realm",
value: link.primary_arm || "Not set",
inline: true,
},
{
name: "👤 User Type",
value: profile?.user_type || "community_member",
inline: true,
},
{
name: "🎭 Discord Roles",
value:
aethexRoles.size > 0
? aethexRoles.map((r) => r.name).join(", ")
: "None assigned yet",
},
{
name: "📋 Expected Roles",
value:
mappings?.length > 0
? mappings.map((m) => m.discord_role).join(", ")
: "No mappings found",
},
)
.setFooter({
text: "Roles are assigned automatically based on your AeThex profile",
});
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error("Verify-role command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("❌ Error")
.setDescription("Failed to verify roles. Please try again.");
await interaction.editReply({ embeds: [embed] });
}
},
};

View file

@ -0,0 +1,85 @@
const {
SlashCommandBuilder,
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
} = require("discord.js");
const { syncRolesAcrossGuilds } = require("../utils/roleManager");
module.exports = {
data: new SlashCommandBuilder()
.setName("verify")
.setDescription("Link your Discord account to your AeThex account"),
async execute(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
try {
const { data: existingLink } = await supabase
.from("discord_links")
.select("*")
.eq("discord_id", interaction.user.id)
.single();
if (existingLink) {
const embed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle("✅ Already Linked")
.setDescription(
`Your Discord account is already linked to AeThex (User ID: ${existingLink.user_id})`,
);
return await interaction.editReply({ embeds: [embed] });
}
// Generate verification code
const verificationCode = Math.random()
.toString(36)
.substring(2, 8)
.toUpperCase();
const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes
// Store verification code with Discord username
await supabase.from("discord_verifications").insert({
discord_id: interaction.user.id,
verification_code: verificationCode,
username: interaction.user.username,
expires_at: expiresAt.toISOString(),
});
const verifyUrl = `https://aethex.dev/discord-verify?code=${verificationCode}`;
const embed = new EmbedBuilder()
.setColor(0x7289da)
.setTitle("🔗 Link Your AeThex Account")
.setDescription(
"Click the button below to link your Discord account to AeThex.",
)
.addFields(
{ name: "⏱️ Expires In", value: "15 minutes" },
{ name: "📝 Verification Code", value: `\`${verificationCode}\`` },
)
.setFooter({ text: "Your security code will expire in 15 minutes" });
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel("Link Account")
.setStyle(ButtonStyle.Link)
.setURL(verifyUrl),
);
await interaction.editReply({ embeds: [embed], components: [row] });
} catch (error) {
console.error("Verify command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("❌ Error")
.setDescription(
"Failed to generate verification code. Please try again.",
);
await interaction.editReply({ embeds: [embed] });
}
},
};

View file

@ -2,6 +2,87 @@ const { REST, Routes } = require('discord.js');
require('dotenv').config();
const commands = [
{
name: 'verify',
description: 'Link your Discord account to your AeThex account',
},
{
name: 'unlink',
description: 'Unlink your Discord account from AeThex',
},
{
name: 'profile',
description: 'View your AeThex profile in Discord',
},
{
name: 'help',
description: 'View all AeThex bot commands and features',
},
{
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: 'stats',
description: 'View your AeThex statistics and activity',
},
{
name: 'set-realm',
description: 'Set your primary AeThex realm/arm',
},
{
name: 'verify-role',
description: 'Check your AeThex-assigned Discord roles',
},
{
name: 'refresh-roles',
description: 'Refresh your Discord roles based on your current AeThex settings',
},
{
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,
},
],
},
{
name: 'ticket',
description: 'Ticket management system',
@ -146,7 +227,7 @@ const rest = new REST({ version: '10' }).setToken(token);
{ body: allCommands }
);
console.log(`Successfully registered ${data.length} commands`);
console.log(`Successfully registered ${data.length} commands:`);
data.forEach(cmd => console.log(` - /${cmd.name}`));
} catch (error) {
console.error('Error registering commands:', error);

View file

@ -0,0 +1,137 @@
const { EmbedBuilder } = require("discord.js");
/**
* Assign Discord role based on user's arm and type
* @param {Guild} guild - Discord guild
* @param {string} discordId - Discord user ID
* @param {string} arm - User's primary arm (labs, gameforge, corp, foundation, devlink)
* @param {object} supabase - Supabase client
* @returns {Promise<boolean>} - Success status
*/
async function assignRoleByArm(guild, discordId, arm, supabase) {
try {
// Fetch guild member
const member = await guild.members.fetch(discordId);
if (!member) {
console.warn(`Member not found: ${discordId}`);
return false;
}
// Get role mapping from Supabase
const { data: mapping, error: mapError } = await supabase
.from("discord_role_mappings")
.select("discord_role")
.eq("arm", arm)
.eq("server_id", guild.id)
.maybeSingle();
if (mapError) {
console.error("Error fetching role mapping:", mapError);
return false;
}
if (!mapping) {
console.warn(
`No role mapping found for arm: ${arm} in server: ${guild.id}`,
);
return false;
}
// Find role by name or ID
let roleToAssign = guild.roles.cache.find(
(r) => r.id === mapping.discord_role || r.name === mapping.discord_role,
);
if (!roleToAssign) {
console.warn(`Role not found: ${mapping.discord_role}`);
return false;
}
// Remove old arm roles
const armRoles = member.roles.cache.filter((role) =>
["Labs", "GameForge", "Corp", "Foundation", "Dev-Link"].some((arm) =>
role.name.includes(arm),
),
);
for (const [, role] of armRoles) {
try {
if (role.id !== roleToAssign.id) {
await member.roles.remove(role);
}
} catch (e) {
console.warn(`Could not remove role ${role.name}: ${e.message}`);
}
}
// Assign new role
if (!member.roles.cache.has(roleToAssign.id)) {
await member.roles.add(roleToAssign);
console.log(
`✅ Assigned role ${roleToAssign.name} to ${member.user.tag}`,
);
return true;
}
return true;
} catch (error) {
console.error("Error assigning role:", error);
return false;
}
}
/**
* Get user's primary arm from Supabase
* @param {string} discordId - Discord user ID
* @param {object} supabase - Supabase client
* @returns {Promise<string>} - Primary arm (labs, gameforge, corp, foundation, devlink)
*/
async function getUserArm(discordId, supabase) {
try {
const { data: link, error } = await supabase
.from("discord_links")
.select("primary_arm")
.eq("discord_id", discordId)
.maybeSingle();
if (error) {
console.error("Error fetching user arm:", error);
return null;
}
return link?.primary_arm || null;
} catch (error) {
console.error("Error getting user arm:", error);
return null;
}
}
/**
* Sync roles for a user across all guilds
* @param {Client} client - Discord client
* @param {string} discordId - Discord user ID
* @param {string} arm - Primary arm
* @param {object} supabase - Supabase client
*/
async function syncRolesAcrossGuilds(client, discordId, arm, supabase) {
try {
for (const [, guild] of client.guilds.cache) {
try {
const member = await guild.members.fetch(discordId);
if (member) {
await assignRoleByArm(guild, discordId, arm, supabase);
}
} catch (e) {
console.warn(`Could not sync roles in guild ${guild.id}: ${e.message}`);
}
}
} catch (error) {
console.error("Error syncing roles across guilds:", error);
}
}
module.exports = {
assignRoleByArm,
getUserArm,
syncRolesAcrossGuilds,
};