diff --git a/aethex-bot/commands/help.js b/aethex-bot/commands/help.js new file mode 100644 index 0000000..324b1dd --- /dev/null +++ b/aethex-bot/commands/help.js @@ -0,0 +1,55 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("help") + .setDescription("View all AeThex bot commands and features"), + + async execute(interaction) { + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("๐Ÿค– AeThex Bot Commands") + .setDescription("Here are all the commands you can use with the AeThex Discord bot.") + .addFields( + { + name: "๐Ÿ”— Account Linking", + value: [ + "`/verify` - Link your Discord account to AeThex", + "`/unlink` - Disconnect your Discord from AeThex", + "`/profile` - View your linked AeThex profile", + ].join("\n"), + }, + { + name: "โš”๏ธ Realm Management", + value: [ + "`/set-realm` - Choose your primary realm (Labs, GameForge, Corp, Foundation, Dev-Link)", + "`/verify-role` - Check your assigned Discord roles", + ].join("\n"), + }, + { + name: "๐Ÿ“Š Community", + value: [ + "`/stats` - View your AeThex statistics and activity", + "`/leaderboard` - See the top contributors", + "`/post` - Create a post in the AeThex community feed", + ].join("\n"), + }, + { + name: "โ„น๏ธ Information", + value: "`/help` - Show this help message", + }, + ) + .addFields({ + name: "๐Ÿ”— Quick Links", + value: [ + "[AeThex Platform](https://aethex.dev)", + "[Creator Directory](https://aethex.dev/creators)", + "[Community Feed](https://aethex.dev/community/feed)", + ].join(" | "), + }) + .setFooter({ text: "AeThex | Build. Create. Connect." }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + }, +}; diff --git a/aethex-bot/commands/leaderboard.js b/aethex-bot/commands/leaderboard.js new file mode 100644 index 0000000..cbc5b01 --- /dev/null +++ b/aethex-bot/commands/leaderboard.js @@ -0,0 +1,155 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("leaderboard") + .setDescription("View the top AeThex contributors") + .addStringOption((option) => + option + .setName("category") + .setDescription("Leaderboard category") + .setRequired(false) + .addChoices( + { name: "๐Ÿ”ฅ Most Active (Posts)", value: "posts" }, + { name: "โค๏ธ Most Liked", value: "likes" }, + { name: "๐ŸŽจ Top Creators", value: "creators" } + ) + ), + + async execute(interaction, supabase) { + await interaction.deferReply(); + + try { + const category = interaction.options.getString("category") || "posts"; + + let leaderboardData = []; + let title = ""; + let emoji = ""; + + if (category === "posts") { + title = "Most Active Posters"; + emoji = "๐Ÿ”ฅ"; + + const { data: posts } = await supabase + .from("community_posts") + .select("user_id") + .not("user_id", "is", null); + + const postCounts = {}; + posts?.forEach((post) => { + postCounts[post.user_id] = (postCounts[post.user_id] || 0) + 1; + }); + + const sortedUsers = Object.entries(postCounts) + .sort(([, a], [, b]) => b - a) + .slice(0, 10); + + for (const [userId, count] of sortedUsers) { + const { data: profile } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", userId) + .single(); + + if (profile) { + leaderboardData.push({ + name: profile.full_name || profile.username || "Anonymous", + value: `${count} posts`, + username: profile.username, + }); + } + } + } else if (category === "likes") { + title = "Most Liked Users"; + emoji = "โค๏ธ"; + + const { data: posts } = await supabase + .from("community_posts") + .select("user_id, likes_count") + .not("user_id", "is", null) + .order("likes_count", { ascending: false }); + + const likeCounts = {}; + posts?.forEach((post) => { + likeCounts[post.user_id] = + (likeCounts[post.user_id] || 0) + (post.likes_count || 0); + }); + + const sortedUsers = Object.entries(likeCounts) + .sort(([, a], [, b]) => b - a) + .slice(0, 10); + + for (const [userId, count] of sortedUsers) { + const { data: profile } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", userId) + .single(); + + if (profile) { + leaderboardData.push({ + name: profile.full_name || profile.username || "Anonymous", + value: `${count} likes received`, + username: profile.username, + }); + } + } + } else if (category === "creators") { + title = "Top Creators"; + emoji = "๐ŸŽจ"; + + const { data: creators } = await supabase + .from("aethex_creators") + .select("user_id, total_projects, verified, featured") + .order("total_projects", { ascending: false }) + .limit(10); + + for (const creator of creators || []) { + const { data: profile } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", creator.user_id) + .single(); + + if (profile) { + const badges = []; + if (creator.verified) badges.push("โœ…"); + if (creator.featured) badges.push("โญ"); + + leaderboardData.push({ + name: profile.full_name || profile.username || "Anonymous", + value: `${creator.total_projects || 0} projects ${badges.join(" ")}`, + username: profile.username, + }); + } + } + } + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle(`${emoji} ${title}`) + .setDescription( + leaderboardData.length > 0 + ? leaderboardData + .map( + (user, index) => + `**${index + 1}.** ${user.name} - ${user.value}` + ) + .join("\n") + : "No data available yet. Be the first to contribute!" + ) + .setFooter({ text: "AeThex Leaderboard | Updated in real-time" }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Leaderboard command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to fetch leaderboard. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/aethex-bot/commands/post.js b/aethex-bot/commands/post.js new file mode 100644 index 0000000..61057e6 --- /dev/null +++ b/aethex-bot/commands/post.js @@ -0,0 +1,144 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + ActionRowBuilder, +} = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("post") + .setDescription("Create a post in the AeThex community feed") + .addStringOption((option) => + option + .setName("content") + .setDescription("Your post content") + .setRequired(true) + .setMaxLength(500) + ) + .addStringOption((option) => + option + .setName("category") + .setDescription("Post category") + .setRequired(false) + .addChoices( + { name: "๐Ÿ’ฌ General", value: "general" }, + { name: "๐Ÿš€ Project Update", value: "project_update" }, + { name: "โ“ Question", value: "question" }, + { name: "๐Ÿ’ก Idea", value: "idea" }, + { name: "๐ŸŽ‰ Announcement", value: "announcement" } + ) + ) + .addAttachmentOption((option) => + option + .setName("image") + .setDescription("Attach an image to your post") + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started." + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", link.user_id) + .single(); + + const content = interaction.options.getString("content"); + const category = interaction.options.getString("category") || "general"; + const attachment = interaction.options.getAttachment("image"); + + let imageUrl = null; + if (attachment && attachment.contentType?.startsWith("image/")) { + imageUrl = attachment.url; + } + + const categoryLabels = { + general: "General", + project_update: "Project Update", + question: "Question", + idea: "Idea", + announcement: "Announcement", + }; + + const { data: post, error } = await supabase + .from("community_posts") + .insert({ + user_id: link.user_id, + content: content, + category: category, + arm_affiliation: link.primary_arm || "general", + image_url: imageUrl, + source: "discord", + discord_message_id: interaction.id, + discord_author_id: interaction.user.id, + discord_author_name: interaction.user.username, + discord_author_avatar: interaction.user.displayAvatarURL(), + }) + .select() + .single(); + + if (error) throw error; + + const successEmbed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Post Created!") + .setDescription(content.length > 100 ? content.slice(0, 100) + "..." : content) + .addFields( + { + name: "๐Ÿ“ Category", + value: categoryLabels[category], + inline: true, + }, + { + name: "โš”๏ธ Realm", + value: link.primary_arm || "general", + inline: true, + } + ); + + if (imageUrl) { + successEmbed.setImage(imageUrl); + } + + successEmbed + .addFields({ + name: "๐Ÿ”— View Post", + value: `[Open in AeThex](https://aethex.dev/community/feed)`, + }) + .setFooter({ text: "Your post is now live on AeThex!" }) + .setTimestamp(); + + await interaction.editReply({ embeds: [successEmbed] }); + } catch (error) { + console.error("Post command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to create post. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/aethex-bot/commands/profile.js b/aethex-bot/commands/profile.js new file mode 100644 index 0000000..035f251 --- /dev/null +++ b/aethex-bot/commands/profile.js @@ -0,0 +1,93 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("profile") + .setDescription("View your AeThex profile in Discord"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("*") + .eq("id", link.user_id) + .single(); + + if (!profile) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Profile Not Found") + .setDescription("Your AeThex profile could not be found."); + + return await interaction.editReply({ embeds: [embed] }); + } + + const armEmojis = { + labs: "๐Ÿงช", + gameforge: "๐ŸŽฎ", + corp: "๐Ÿ’ผ", + foundation: "๐Ÿค", + devlink: "๐Ÿ’ป", + }; + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle(`${profile.full_name || "AeThex User"}'s Profile`) + .setThumbnail( + profile.avatar_url || "https://aethex.dev/placeholder.svg", + ) + .addFields( + { + name: "๐Ÿ‘ค Username", + value: profile.username || "N/A", + inline: true, + }, + { + name: `${armEmojis[link.primary_arm] || "โš”๏ธ"} Primary Realm`, + value: link.primary_arm || "Not set", + inline: true, + }, + { + name: "๐Ÿ“Š Role", + value: profile.user_type || "community_member", + inline: true, + }, + { name: "๐Ÿ“ Bio", value: profile.bio || "No bio set", inline: false }, + ) + .addFields({ + name: "๐Ÿ”— Links", + value: `[Visit Full Profile](https://aethex.dev/creators/${profile.username})`, + }) + .setFooter({ text: "AeThex | Your Web3 Creator Hub" }); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Profile command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to fetch profile. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/aethex-bot/commands/refresh-roles.js b/aethex-bot/commands/refresh-roles.js new file mode 100644 index 0000000..459bd79 --- /dev/null +++ b/aethex-bot/commands/refresh-roles.js @@ -0,0 +1,72 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const { assignRoleByArm, getUserArm } = require("../utils/roleManager"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("refresh-roles") + .setDescription( + "Refresh your Discord roles based on your current AeThex settings", + ), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + // Check if user is linked + const { data: link } = await supabase + .from("discord_links") + .select("primary_arm") + .eq("discord_id", interaction.user.id) + .maybeSingle(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + if (!link.primary_arm) { + const embed = new EmbedBuilder() + .setColor(0xffaa00) + .setTitle("โš ๏ธ No Realm Set") + .setDescription( + "You haven't set your primary realm yet.\nUse `/set-realm` to choose one.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + // Assign role based on current primary arm + const roleAssigned = await assignRoleByArm( + interaction.guild, + interaction.user.id, + link.primary_arm, + supabase, + ); + + const embed = new EmbedBuilder() + .setColor(roleAssigned ? 0x00ff00 : 0xffaa00) + .setTitle("โœ… Roles Refreshed") + .setDescription( + roleAssigned + ? `Your Discord roles have been synced with your AeThex account.\n\nPrimary Realm: **${link.primary_arm}**` + : `Your roles could not be automatically assigned.\n\nPrimary Realm: **${link.primary_arm}**\n\nโš ๏ธ Please contact an admin to set up the role mapping for this server.`, + ); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Refresh-roles command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to refresh roles. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/aethex-bot/commands/set-realm.js b/aethex-bot/commands/set-realm.js new file mode 100644 index 0000000..c1af120 --- /dev/null +++ b/aethex-bot/commands/set-realm.js @@ -0,0 +1,139 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + StringSelectMenuBuilder, + ActionRowBuilder, +} = require("discord.js"); +const { assignRoleByArm } = require("../utils/roleManager"); + +const REALMS = [ + { value: "labs", label: "๐Ÿงช Labs", description: "Research & Development" }, + { + value: "gameforge", + label: "๐ŸŽฎ GameForge", + description: "Game Development", + }, + { value: "corp", label: "๐Ÿ’ผ Corp", description: "Enterprise Solutions" }, + { + value: "foundation", + label: "๐Ÿค Foundation", + description: "Community & Education", + }, + { + value: "devlink", + label: "๐Ÿ’ป Dev-Link", + description: "Professional Networking", + }, +]; + +module.exports = { + data: new SlashCommandBuilder() + .setName("set-realm") + .setDescription("Set your primary AeThex realm/arm"), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const select = new StringSelectMenuBuilder() + .setCustomId("select_realm") + .setPlaceholder("Choose your primary realm") + .addOptions( + REALMS.map((realm) => ({ + label: realm.label, + description: realm.description, + value: realm.value, + default: realm.value === link.primary_arm, + })), + ); + + const row = new ActionRowBuilder().addComponents(select); + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("โš”๏ธ Choose Your Realm") + .setDescription( + "Select your primary AeThex realm. This determines your main Discord role.", + ) + .addFields({ + name: "Current Realm", + value: link.primary_arm || "Not set", + }); + + await interaction.editReply({ embeds: [embed], components: [row] }); + + const filter = (i) => + i.user.id === interaction.user.id && i.customId === "select_realm"; + const collector = interaction.channel.createMessageComponentCollector({ + filter, + time: 60000, + }); + + collector.on("collect", async (i) => { + const selectedRealm = i.values[0]; + + await supabase + .from("discord_links") + .update({ primary_arm: selectedRealm }) + .eq("discord_id", interaction.user.id); + + const realm = REALMS.find((r) => r.value === selectedRealm); + + // Assign Discord role based on selected realm + const roleAssigned = await assignRoleByArm( + interaction.guild, + interaction.user.id, + selectedRealm, + supabase, + ); + + const roleStatus = roleAssigned + ? "โœ… Discord role assigned!" + : "โš ๏ธ No role mapping found for this realm in this server."; + + const confirmEmbed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Realm Set") + .setDescription( + `Your primary realm is now **${realm.label}**\n\n${roleStatus}`, + ); + + await i.update({ embeds: [confirmEmbed], components: [] }); + }); + + collector.on("end", (collected) => { + if (collected.size === 0) { + interaction.editReply({ + content: "Realm selection timed out.", + components: [], + }); + } + }); + } catch (error) { + console.error("Set-realm command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to update realm. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/aethex-bot/commands/stats.js b/aethex-bot/commands/stats.js new file mode 100644 index 0000000..fe9814b --- /dev/null +++ b/aethex-bot/commands/stats.js @@ -0,0 +1,140 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("stats") + .setDescription("View your AeThex statistics and activity"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm, created_at") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started." + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("*") + .eq("id", link.user_id) + .single(); + + const { count: postCount } = await supabase + .from("community_posts") + .select("*", { count: "exact", head: true }) + .eq("user_id", link.user_id); + + const { count: likeCount } = await supabase + .from("community_likes") + .select("*", { count: "exact", head: true }) + .eq("user_id", link.user_id); + + const { count: commentCount } = await supabase + .from("community_comments") + .select("*", { count: "exact", head: true }) + .eq("user_id", link.user_id); + + const { data: creatorProfile } = await supabase + .from("aethex_creators") + .select("verified, featured, total_projects") + .eq("user_id", link.user_id) + .single(); + + const armEmojis = { + labs: "๐Ÿงช", + gameforge: "๐ŸŽฎ", + corp: "๐Ÿ’ผ", + foundation: "๐Ÿค", + devlink: "๐Ÿ’ป", + }; + + const linkedDate = new Date(link.created_at); + const daysSinceLinked = Math.floor( + (Date.now() - linkedDate.getTime()) / (1000 * 60 * 60 * 24) + ); + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle(`๐Ÿ“Š ${profile?.full_name || interaction.user.username}'s Stats`) + .setThumbnail(profile?.avatar_url || interaction.user.displayAvatarURL()) + .addFields( + { + name: `${armEmojis[link.primary_arm] || "โš”๏ธ"} Primary Realm`, + value: link.primary_arm || "Not set", + inline: true, + }, + { + name: "๐Ÿ‘ค Account Type", + value: profile?.user_type || "community_member", + inline: true, + }, + { + name: "๐Ÿ“… Days Linked", + value: `${daysSinceLinked} days`, + inline: true, + } + ) + .addFields( + { + name: "๐Ÿ“ Posts", + value: `${postCount || 0}`, + inline: true, + }, + { + name: "โค๏ธ Likes Given", + value: `${likeCount || 0}`, + inline: true, + }, + { + name: "๐Ÿ’ฌ Comments", + value: `${commentCount || 0}`, + inline: true, + } + ); + + if (creatorProfile) { + embed.addFields({ + name: "๐ŸŽจ Creator Status", + value: [ + creatorProfile.verified ? "โœ… Verified Creator" : "โณ Pending Verification", + creatorProfile.featured ? "โญ Featured" : "", + `๐Ÿ“ ${creatorProfile.total_projects || 0} Projects`, + ] + .filter(Boolean) + .join("\n"), + }); + } + + embed + .addFields({ + name: "๐Ÿ”— Full Profile", + value: `[View on AeThex](https://aethex.dev/creators/${profile?.username || link.user_id})`, + }) + .setFooter({ text: "AeThex | Your Creative Hub" }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Stats command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to fetch stats. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/aethex-bot/commands/unlink.js b/aethex-bot/commands/unlink.js new file mode 100644 index 0000000..ac06d2a --- /dev/null +++ b/aethex-bot/commands/unlink.js @@ -0,0 +1,75 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("unlink") + .setDescription("Unlink your Discord account from AeThex"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("*") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โ„น๏ธ Not Linked") + .setDescription("Your Discord account is not linked to AeThex."); + + return await interaction.editReply({ embeds: [embed] }); + } + + // Delete the link + await supabase + .from("discord_links") + .delete() + .eq("discord_id", interaction.user.id); + + // Remove Discord roles from user + const guild = interaction.guild; + const member = await guild.members.fetch(interaction.user.id); + + // Find and remove all AeThex-related roles + const rolesToRemove = member.roles.cache.filter( + (role) => + role.name.includes("Labs") || + role.name.includes("GameForge") || + role.name.includes("Corp") || + role.name.includes("Foundation") || + role.name.includes("Dev-Link") || + role.name.includes("Premium") || + role.name.includes("Creator"), + ); + + for (const [, role] of rolesToRemove) { + try { + await member.roles.remove(role); + } catch (e) { + console.warn(`Could not remove role ${role.name}`); + } + } + + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Account Unlinked") + .setDescription( + "Your Discord account has been unlinked from AeThex.\nAll associated roles have been removed.", + ); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Unlink command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to unlink account. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/aethex-bot/commands/verify-role.js b/aethex-bot/commands/verify-role.js new file mode 100644 index 0000000..1b7e6b9 --- /dev/null +++ b/aethex-bot/commands/verify-role.js @@ -0,0 +1,97 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("verify-role") + .setDescription("Check your AeThex-assigned Discord roles"), + + async execute(interaction, supabase) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from("discord_links") + .select("user_id, primary_arm") + .eq("discord_id", interaction.user.id) + .single(); + + if (!link) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("โŒ Not Linked") + .setDescription( + "You must link your Discord account to AeThex first.\nUse `/verify` to get started.", + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + const { data: profile } = await supabase + .from("user_profiles") + .select("user_type") + .eq("id", link.user_id) + .single(); + + const { data: mappings } = await supabase + .from("discord_role_mappings") + .select("discord_role") + .eq("arm", link.primary_arm) + .eq("user_type", profile?.user_type || "community_member"); + + const member = await interaction.guild.members.fetch(interaction.user.id); + const aethexRoles = member.roles.cache.filter( + (role) => + role.name.includes("Labs") || + role.name.includes("GameForge") || + role.name.includes("Corp") || + role.name.includes("Foundation") || + role.name.includes("Dev-Link") || + role.name.includes("Premium") || + role.name.includes("Creator"), + ); + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("๐Ÿ” Your AeThex Roles") + .addFields( + { + name: "โš”๏ธ Primary Realm", + value: link.primary_arm || "Not set", + inline: true, + }, + { + name: "๐Ÿ‘ค User Type", + value: profile?.user_type || "community_member", + inline: true, + }, + { + name: "๐ŸŽญ Discord Roles", + value: + aethexRoles.size > 0 + ? aethexRoles.map((r) => r.name).join(", ") + : "None assigned yet", + }, + { + name: "๐Ÿ“‹ Expected Roles", + value: + mappings?.length > 0 + ? mappings.map((m) => m.discord_role).join(", ") + : "No mappings found", + }, + ) + .setFooter({ + text: "Roles are assigned automatically based on your AeThex profile", + }); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error("Verify-role command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription("Failed to verify roles. Please try again."); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/aethex-bot/commands/verify.js b/aethex-bot/commands/verify.js new file mode 100644 index 0000000..d9f30e7 --- /dev/null +++ b/aethex-bot/commands/verify.js @@ -0,0 +1,85 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, +} = require("discord.js"); +const { syncRolesAcrossGuilds } = require("../utils/roleManager"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("verify") + .setDescription("Link your Discord account to your AeThex account"), + + async execute(interaction, supabase, client) { + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: existingLink } = await supabase + .from("discord_links") + .select("*") + .eq("discord_id", interaction.user.id) + .single(); + + if (existingLink) { + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle("โœ… Already Linked") + .setDescription( + `Your Discord account is already linked to AeThex (User ID: ${existingLink.user_id})`, + ); + + return await interaction.editReply({ embeds: [embed] }); + } + + // Generate verification code + const verificationCode = Math.random() + .toString(36) + .substring(2, 8) + .toUpperCase(); + const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes + + // Store verification code with Discord username + await supabase.from("discord_verifications").insert({ + discord_id: interaction.user.id, + verification_code: verificationCode, + username: interaction.user.username, + expires_at: expiresAt.toISOString(), + }); + + const verifyUrl = `https://aethex.dev/discord-verify?code=${verificationCode}`; + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle("๐Ÿ”— Link Your AeThex Account") + .setDescription( + "Click the button below to link your Discord account to AeThex.", + ) + .addFields( + { name: "โฑ๏ธ Expires In", value: "15 minutes" }, + { name: "๐Ÿ“ Verification Code", value: `\`${verificationCode}\`` }, + ) + .setFooter({ text: "Your security code will expire in 15 minutes" }); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("Link Account") + .setStyle(ButtonStyle.Link) + .setURL(verifyUrl), + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); + } catch (error) { + console.error("Verify command error:", error); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("โŒ Error") + .setDescription( + "Failed to generate verification code. Please try again.", + ); + + await interaction.editReply({ embeds: [embed] }); + } + }, +}; diff --git a/aethex-bot/scripts/register-commands.js b/aethex-bot/scripts/register-commands.js index 03076a3..441715c 100644 --- a/aethex-bot/scripts/register-commands.js +++ b/aethex-bot/scripts/register-commands.js @@ -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); diff --git a/aethex-bot/utils/roleManager.js b/aethex-bot/utils/roleManager.js new file mode 100644 index 0000000..27be439 --- /dev/null +++ b/aethex-bot/utils/roleManager.js @@ -0,0 +1,137 @@ +const { EmbedBuilder } = require("discord.js"); + +/** + * Assign Discord role based on user's arm and type + * @param {Guild} guild - Discord guild + * @param {string} discordId - Discord user ID + * @param {string} arm - User's primary arm (labs, gameforge, corp, foundation, devlink) + * @param {object} supabase - Supabase client + * @returns {Promise} - Success status + */ +async function assignRoleByArm(guild, discordId, arm, supabase) { + try { + // Fetch guild member + const member = await guild.members.fetch(discordId); + if (!member) { + console.warn(`Member not found: ${discordId}`); + return false; + } + + // Get role mapping from Supabase + const { data: mapping, error: mapError } = await supabase + .from("discord_role_mappings") + .select("discord_role") + .eq("arm", arm) + .eq("server_id", guild.id) + .maybeSingle(); + + if (mapError) { + console.error("Error fetching role mapping:", mapError); + return false; + } + + if (!mapping) { + console.warn( + `No role mapping found for arm: ${arm} in server: ${guild.id}`, + ); + return false; + } + + // Find role by name or ID + let roleToAssign = guild.roles.cache.find( + (r) => r.id === mapping.discord_role || r.name === mapping.discord_role, + ); + + if (!roleToAssign) { + console.warn(`Role not found: ${mapping.discord_role}`); + return false; + } + + // Remove old arm roles + const armRoles = member.roles.cache.filter((role) => + ["Labs", "GameForge", "Corp", "Foundation", "Dev-Link"].some((arm) => + role.name.includes(arm), + ), + ); + + for (const [, role] of armRoles) { + try { + if (role.id !== roleToAssign.id) { + await member.roles.remove(role); + } + } catch (e) { + console.warn(`Could not remove role ${role.name}: ${e.message}`); + } + } + + // Assign new role + if (!member.roles.cache.has(roleToAssign.id)) { + await member.roles.add(roleToAssign); + console.log( + `โœ… Assigned role ${roleToAssign.name} to ${member.user.tag}`, + ); + return true; + } + + return true; + } catch (error) { + console.error("Error assigning role:", error); + return false; + } +} + +/** + * Get user's primary arm from Supabase + * @param {string} discordId - Discord user ID + * @param {object} supabase - Supabase client + * @returns {Promise} - Primary arm (labs, gameforge, corp, foundation, devlink) + */ +async function getUserArm(discordId, supabase) { + try { + const { data: link, error } = await supabase + .from("discord_links") + .select("primary_arm") + .eq("discord_id", discordId) + .maybeSingle(); + + if (error) { + console.error("Error fetching user arm:", error); + return null; + } + + return link?.primary_arm || null; + } catch (error) { + console.error("Error getting user arm:", error); + return null; + } +} + +/** + * Sync roles for a user across all guilds + * @param {Client} client - Discord client + * @param {string} discordId - Discord user ID + * @param {string} arm - Primary arm + * @param {object} supabase - Supabase client + */ +async function syncRolesAcrossGuilds(client, discordId, arm, supabase) { + try { + for (const [, guild] of client.guilds.cache) { + try { + const member = await guild.members.fetch(discordId); + if (member) { + await assignRoleByArm(guild, discordId, arm, supabase); + } + } catch (e) { + console.warn(`Could not sync roles in guild ${guild.id}: ${e.message}`); + } + } + } catch (error) { + console.error("Error syncing roles across guilds:", error); + } +} + +module.exports = { + assignRoleByArm, + getUserArm, + syncRolesAcrossGuilds, +};