diff --git a/aethex-bot/bot.js b/aethex-bot/bot.js index 8c990ac..39ec557 100644 --- a/aethex-bot/bot.js +++ b/aethex-bot/bot.js @@ -7,7 +7,6 @@ const { EmbedBuilder, ChannelType, PermissionFlagsBits, - AuditLogEvent, } = require("discord.js"); const { createClient } = require("@supabase/supabase-js"); const http = require("http"); @@ -23,6 +22,13 @@ if (!token) { process.exit(1); } +if (!clientId) { + console.error("Missing DISCORD_CLIENT_ID environment variable"); + process.exit(1); +} + +console.log("[Token] Bot token loaded (length: " + token.length + " chars)"); + const client = new Client({ intents: [ GatewayIntentBits.Guilds, @@ -41,6 +47,8 @@ if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) { process.env.SUPABASE_SERVICE_ROLE ); console.log("Supabase connected"); +} else { + console.log("Supabase not configured - community features will be limited"); } client.commands = new Collection(); @@ -87,7 +95,6 @@ if (fs.existsSync(sentinelPath)) { const heatMap = new Map(); const HEAT_THRESHOLD = 3; const HEAT_WINDOW_MS = 10000; -const DANGEROUS_ACTIONS = ['CHANNEL_DELETE', 'ROLE_DELETE', 'MEMBER_BAN_ADD', 'MEMBER_KICK']; const whitelistedUsers = (process.env.WHITELISTED_USERS || '').split(',').filter(Boolean); function addHeat(userId, action) { @@ -153,12 +160,23 @@ async function sendAlert(message, embed = null) { } client.sendAlert = sendAlert; +let feedSyncModule = null; +try { + feedSyncModule = require("./listeners/feedSync"); +} catch (e) { + console.log("Feed sync module not available"); +} + client.once("ready", () => { console.log(`Bot logged in as ${client.user.tag}`); console.log(`Watching ${client.guilds.cache.size} server(s)`); client.user.setActivity("Protecting the Federation", { type: 3 }); + if (feedSyncModule && feedSyncModule.setupFeedListener && supabase) { + feedSyncModule.setupFeedListener(client); + } + sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`); }); @@ -190,7 +208,6 @@ client.on("interactionCreate", async (interaction) => { if (action === 'ticket') { const ticketAction = params[0]; if (ticketAction === 'close') { - const ticketChannelId = params[1]; try { const channel = interaction.channel; if (channel && channel.type === ChannelType.GuildText) { @@ -206,6 +223,7 @@ client.on("interactionCreate", async (interaction) => { }); const healthPort = process.env.HEALTH_PORT || 8080; + http.createServer((req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Content-Type", "application/json"); @@ -218,6 +236,7 @@ http.createServer((req, res) => { commands: client.commands.size, uptime: Math.floor(process.uptime()), heatMapSize: heatMap.size, + supabaseConnected: !!supabase, timestamp: new Date().toISOString(), })); return; diff --git a/aethex-bot/commands/admin.js b/aethex-bot/commands/admin.js index 4573f7b..64a4c0b 100644 --- a/aethex-bot/commands/admin.js +++ b/aethex-bot/commands/admin.js @@ -57,7 +57,8 @@ module.exports = { { name: 'Commands', value: `${commandCount}`, inline: true }, { name: 'Uptime', value: `${hours}h ${minutes}m`, inline: true }, { name: 'Active Tickets', value: `${client.activeTickets.size}`, inline: true }, - { name: 'Heat Map Size', value: `${client.heatMap.size}`, inline: true } + { name: 'Heat Map Size', value: `${client.heatMap.size}`, inline: true }, + { name: 'Supabase', value: supabase ? 'Connected' : 'Not configured', inline: true } ) .setTimestamp(); diff --git a/aethex-bot/commands/federation.js b/aethex-bot/commands/federation.js index a55e3ba..1d07978 100644 --- a/aethex-bot/commands/federation.js +++ b/aethex-bot/commands/federation.js @@ -3,22 +3,22 @@ const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('disc module.exports = { data: new SlashCommandBuilder() .setName('federation') - .setDescription('Manage cross-server role sync') - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDescription('Manage cross-server role synchronization') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles) .addSubcommand(subcommand => subcommand .setName('link') - .setDescription('Link a role for cross-server syncing') + .setDescription('Link a role for cross-server sync') .addRoleOption(option => option.setName('role') - .setDescription('Role to sync across realms') + .setDescription('Role to sync across servers') .setRequired(true) ) ) .addSubcommand(subcommand => subcommand .setName('unlink') - .setDescription('Remove a role from cross-server syncing') + .setDescription('Remove a role from sync') .addRoleOption(option => option.setName('role') .setDescription('Role to remove from sync') @@ -37,78 +37,76 @@ module.exports = { if (subcommand === 'link') { const role = interaction.options.getRole('role'); - if (client.federationMappings.has(role.id)) { - return interaction.reply({ - content: `${role} is already linked for federation sync.`, - ephemeral: true, - }); - } - client.federationMappings.set(role.id, { name: role.name, - guildId: interaction.guild.id, - createdAt: Date.now(), + guildId: interaction.guildId, + guildName: interaction.guild.name, + linkedAt: Date.now(), }); const embed = new EmbedBuilder() .setColor(0x00ff00) .setTitle('Role Linked') - .setDescription(`${role} will now sync across all realm servers.`) + .setDescription(`${role} is now linked for federation sync.`) .addFields( - { name: 'Role Name', value: role.name, inline: true }, - { name: 'Source Guild', value: interaction.guild.name, inline: true } + { name: 'Role ID', value: role.id, inline: true }, + { name: 'Server', value: interaction.guild.name, inline: true } ) - .setFooter({ text: 'Users with this role will receive it in all connected realms.' }) .setTimestamp(); await interaction.reply({ embeds: [embed] }); - - client.sendAlert(`Federation: Role ${role.name} linked by ${interaction.user.tag}`); } if (subcommand === 'unlink') { const role = interaction.options.getRole('role'); - if (!client.federationMappings.has(role.id)) { - return interaction.reply({ - content: `${role} is not linked for federation sync.`, - ephemeral: true, - }); + if (client.federationMappings.has(role.id)) { + client.federationMappings.delete(role.id); + + const embed = new EmbedBuilder() + .setColor(0xff6600) + .setTitle('Role Unlinked') + .setDescription(`${role} has been removed from federation sync.`) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + } else { + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle('Not Found') + .setDescription(`${role} is not currently linked.`) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral: true }); } - - client.federationMappings.delete(role.id); - - const embed = new EmbedBuilder() - .setColor(0xff0000) - .setTitle('Role Unlinked') - .setDescription(`${role} will no longer sync across realm servers.`) - .setTimestamp(); - - await interaction.reply({ embeds: [embed] }); - - client.sendAlert(`Federation: Role ${role.name} unlinked by ${interaction.user.tag}`); } if (subcommand === 'list') { const mappings = [...client.federationMappings.entries()]; if (mappings.length === 0) { - return interaction.reply({ - content: 'No roles are currently linked for federation sync.', - ephemeral: true, - }); + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('Federation Roles') + .setDescription('No roles are currently linked for federation sync.\nUse `/federation link` to add roles.') + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + return; } - const roleList = mappings.map(([roleId, data]) => `<@&${roleId}> - Added `).join('\n'); + const roleList = mappings.map(([roleId, data]) => + `<@&${roleId}> - ${data.guildName}` + ).join('\n'); const embed = new EmbedBuilder() .setColor(0x7c3aed) - .setTitle('Federation Linked Roles') + .setTitle('Federation Roles') .setDescription(roleList) .setFooter({ text: `${mappings.length} role(s) linked` }) .setTimestamp(); - await interaction.reply({ embeds: [embed], ephemeral: true }); + await interaction.reply({ embeds: [embed] }); } }, }; diff --git a/aethex-bot/commands/leaderboard.js b/aethex-bot/commands/leaderboard.js index cbc5b01..2f6fcfd 100644 --- a/aethex-bot/commands/leaderboard.js +++ b/aethex-bot/commands/leaderboard.js @@ -19,6 +19,14 @@ module.exports = { async execute(interaction, supabase) { await interaction.deferReply(); + if (!supabase) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("⚠️ Feature Unavailable") + .setDescription("Leaderboard is not configured. Contact an administrator."); + return await interaction.editReply({ embeds: [embed] }); + } + try { const category = interaction.options.getString("category") || "posts"; diff --git a/aethex-bot/commands/post.js b/aethex-bot/commands/post.js index 61057e6..04c91dd 100644 --- a/aethex-bot/commands/post.js +++ b/aethex-bot/commands/post.js @@ -41,6 +41,14 @@ module.exports = { async execute(interaction, supabase, client) { await interaction.deferReply({ ephemeral: true }); + if (!supabase) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("⚠️ Feature Unavailable") + .setDescription("Posting is not configured. Contact an administrator."); + return await interaction.editReply({ embeds: [embed] }); + } + try { const { data: link } = await supabase .from("discord_links") diff --git a/aethex-bot/commands/profile.js b/aethex-bot/commands/profile.js index 035f251..4faab7d 100644 --- a/aethex-bot/commands/profile.js +++ b/aethex-bot/commands/profile.js @@ -8,6 +8,14 @@ module.exports = { async execute(interaction, supabase) { await interaction.deferReply({ ephemeral: true }); + if (!supabase) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("⚠️ Feature Unavailable") + .setDescription("Profile features are not configured. Contact an administrator."); + return await interaction.editReply({ embeds: [embed] }); + } + try { const { data: link } = await supabase .from("discord_links") diff --git a/aethex-bot/commands/refresh-roles.js b/aethex-bot/commands/refresh-roles.js index 459bd79..a159d11 100644 --- a/aethex-bot/commands/refresh-roles.js +++ b/aethex-bot/commands/refresh-roles.js @@ -11,8 +11,15 @@ module.exports = { async execute(interaction, supabase, client) { await interaction.deferReply({ ephemeral: true }); + if (!supabase) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("⚠️ Feature Unavailable") + .setDescription("Role sync is not configured. Contact an administrator."); + return await interaction.editReply({ embeds: [embed] }); + } + try { - // Check if user is linked const { data: link } = await supabase .from("discord_links") .select("primary_arm") diff --git a/aethex-bot/commands/set-realm.js b/aethex-bot/commands/set-realm.js index c1af120..f8e8204 100644 --- a/aethex-bot/commands/set-realm.js +++ b/aethex-bot/commands/set-realm.js @@ -34,6 +34,14 @@ module.exports = { async execute(interaction, supabase, client) { await interaction.deferReply({ ephemeral: true }); + if (!supabase) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("⚠️ Feature Unavailable") + .setDescription("Realm settings are not configured. Contact an administrator."); + return await interaction.editReply({ embeds: [embed] }); + } + try { const { data: link } = await supabase .from("discord_links") diff --git a/aethex-bot/commands/stats.js b/aethex-bot/commands/stats.js index fe9814b..6931f9d 100644 --- a/aethex-bot/commands/stats.js +++ b/aethex-bot/commands/stats.js @@ -8,6 +8,14 @@ module.exports = { async execute(interaction, supabase) { await interaction.deferReply({ ephemeral: true }); + if (!supabase) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("⚠️ Feature Unavailable") + .setDescription("Stats are not configured. Contact an administrator."); + return await interaction.editReply({ embeds: [embed] }); + } + try { const { data: link } = await supabase .from("discord_links") diff --git a/aethex-bot/commands/ticket.js b/aethex-bot/commands/ticket.js index 8a9fb2b..d0cf286 100644 --- a/aethex-bot/commands/ticket.js +++ b/aethex-bot/commands/ticket.js @@ -3,15 +3,15 @@ const { SlashCommandBuilder, EmbedBuilder, ChannelType, PermissionFlagsBits, Act module.exports = { data: new SlashCommandBuilder() .setName('ticket') - .setDescription('Ticket management system') + .setDescription('Support ticket system') .addSubcommand(subcommand => subcommand .setName('create') - .setDescription('Create a new support ticket') + .setDescription('Create a support ticket') .addStringOption(option => option.setName('reason') - .setDescription('Brief reason for opening this ticket') - .setRequired(true) + .setDescription('Reason for the ticket') + .setRequired(false) ) ) .addSubcommand(subcommand => @@ -24,103 +24,114 @@ module.exports = { const subcommand = interaction.options.getSubcommand(); if (subcommand === 'create') { - const reason = interaction.options.getString('reason'); - const guild = interaction.guild; - const user = interaction.user; + const reason = interaction.options.getString('reason') || 'No reason provided'; + + const existingTicket = [...client.activeTickets.entries()].find( + ([, data]) => data.userId === interaction.user.id && data.guildId === interaction.guildId + ); - const existingTicket = client.activeTickets.get(user.id); if (existingTicket) { - return interaction.reply({ - content: `You already have an open ticket: <#${existingTicket}>`, - ephemeral: true, - }); + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle('Ticket Already Exists') + .setDescription(`You already have an open ticket: <#${existingTicket[0]}>`) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + return; } try { - const ticketChannel = await guild.channels.create({ - name: `ticket-${user.username}`, + const ticketChannel = await interaction.guild.channels.create({ + name: `ticket-${interaction.user.username}`, type: ChannelType.GuildText, permissionOverwrites: [ { - id: guild.id, + id: interaction.guild.id, deny: [PermissionFlagsBits.ViewChannel], }, { - id: user.id, + id: interaction.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages], }, - { - id: client.user.id, - allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ManageChannels], - }, ], }); - client.activeTickets.set(user.id, ticketChannel.id); + client.activeTickets.set(ticketChannel.id, { + userId: interaction.user.id, + guildId: interaction.guildId, + reason: reason, + createdAt: Date.now(), + }); - const closeButton = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId(`ticket_close_${ticketChannel.id}`) - .setLabel('Close Ticket') - .setStyle(ButtonStyle.Danger) - ); + const closeButton = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ticket_close_${ticketChannel.id}`) + .setLabel('Close Ticket') + .setStyle(ButtonStyle.Danger) + ); const ticketEmbed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('Support Ticket') - .setDescription(`Ticket created by ${user}`) + .setDescription(`Ticket created by ${interaction.user}`) .addFields( { name: 'Reason', value: reason }, - { name: 'User ID', value: user.id, inline: true }, - { name: 'Created', value: ``, inline: true } + { name: 'Created', value: `` } ) - .setFooter({ text: 'A staff member will assist you shortly.' }); + .setFooter({ text: 'Click the button below to close this ticket' }) + .setTimestamp(); await ticketChannel.send({ embeds: [ticketEmbed], components: [closeButton] }); - await interaction.reply({ - content: `Ticket created: ${ticketChannel}`, - ephemeral: true, - }); - - client.sendAlert(`New ticket opened by ${user.tag}: ${reason}`); + const successEmbed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle('Ticket Created') + .setDescription(`Your ticket has been created: ${ticketChannel}`) + .setTimestamp(); + await interaction.reply({ embeds: [successEmbed], ephemeral: true }); } catch (error) { - console.error('[Ticket] Create error:', error); - await interaction.reply({ - content: 'Failed to create ticket. Please try again.', - ephemeral: true, - }); + console.error('Ticket creation error:', error); + const errorEmbed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle('Error') + .setDescription('Failed to create ticket. I may not have the required permissions.') + .setTimestamp(); + + await interaction.reply({ embeds: [errorEmbed], ephemeral: true }); } } if (subcommand === 'close') { - const channel = interaction.channel; + const ticketData = client.activeTickets.get(interaction.channelId); - if (!channel.name.startsWith('ticket-')) { - return interaction.reply({ - content: 'This command can only be used in ticket channels.', - ephemeral: true, - }); + if (!ticketData) { + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle('Not a Ticket') + .setDescription('This channel is not a support ticket.') + .setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + return; } - const userId = [...client.activeTickets.entries()].find(([k, v]) => v === channel.id)?.[0]; - if (userId) { - client.activeTickets.delete(userId); - } + const embed = new EmbedBuilder() + .setColor(0xff6600) + .setTitle('Closing Ticket') + .setDescription('This ticket will be closed in 5 seconds...') + .setTimestamp(); - await interaction.reply({ - content: 'Closing ticket in 5 seconds...', - }); + await interaction.reply({ embeds: [embed] }); - client.sendAlert(`Ticket ${channel.name} closed by ${interaction.user.tag}`); + client.activeTickets.delete(interaction.channelId); setTimeout(async () => { try { - await channel.delete(); - } catch (err) { - console.error('[Ticket] Delete error:', err); + await interaction.channel.delete(); + } catch (e) { + console.error('Failed to delete ticket channel:', e); } }, 5000); } diff --git a/aethex-bot/commands/unlink.js b/aethex-bot/commands/unlink.js index ac06d2a..4812ea6 100644 --- a/aethex-bot/commands/unlink.js +++ b/aethex-bot/commands/unlink.js @@ -8,6 +8,14 @@ module.exports = { async execute(interaction, supabase) { await interaction.deferReply({ ephemeral: true }); + if (!supabase) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("⚠️ Feature Unavailable") + .setDescription("Account unlinking is not configured. Contact an administrator."); + return await interaction.editReply({ embeds: [embed] }); + } + try { const { data: link } = await supabase .from("discord_links") diff --git a/aethex-bot/commands/verify-role.js b/aethex-bot/commands/verify-role.js index 1b7e6b9..1677d58 100644 --- a/aethex-bot/commands/verify-role.js +++ b/aethex-bot/commands/verify-role.js @@ -8,6 +8,14 @@ module.exports = { async execute(interaction, supabase) { await interaction.deferReply({ ephemeral: true }); + if (!supabase) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("⚠️ Feature Unavailable") + .setDescription("Role verification is not configured. Contact an administrator."); + return await interaction.editReply({ embeds: [embed] }); + } + try { const { data: link } = await supabase .from("discord_links") diff --git a/aethex-bot/commands/verify.js b/aethex-bot/commands/verify.js index 4e5e6c4..a4bbf1b 100644 --- a/aethex-bot/commands/verify.js +++ b/aethex-bot/commands/verify.js @@ -15,6 +15,14 @@ module.exports = { async execute(interaction, supabase, client) { await interaction.deferReply({ ephemeral: true }); + if (!supabase) { + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle("⚠️ Feature Unavailable") + .setDescription("Account linking is not configured. Contact an administrator."); + return await interaction.editReply({ embeds: [embed] }); + } + try { const { data: existingLink } = await supabase .from("discord_links") @@ -40,10 +48,11 @@ module.exports = { .toUpperCase(); const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes - // Store verification code + // 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(), }); diff --git a/aethex-bot/events/guildMemberUpdate.js b/aethex-bot/events/guildMemberUpdate.js deleted file mode 100644 index 0e0c6b8..0000000 --- a/aethex-bot/events/guildMemberUpdate.js +++ /dev/null @@ -1,80 +0,0 @@ -const { EmbedBuilder } = require('discord.js'); - -module.exports = { - name: 'guildMemberUpdate', - async execute(oldMember, newMember, client) { - try { - const addedRoles = newMember.roles.cache.filter(role => !oldMember.roles.cache.has(role.id)); - const removedRoles = oldMember.roles.cache.filter(role => !newMember.roles.cache.has(role.id)); - - if (addedRoles.size === 0 && removedRoles.size === 0) return; - - const REALM_GUILDS = client.REALM_GUILDS; - const realmGuildIds = Object.values(REALM_GUILDS).filter(Boolean); - - if (!realmGuildIds.includes(newMember.guild.id)) return; - - const federationMappings = client.federationMappings; - - for (const [roleId, role] of addedRoles) { - const mapping = federationMappings.get(roleId); - if (!mapping) continue; - - console.log(`[Federation] Syncing role ${role.name} for ${newMember.user.tag} across realms`); - - for (const targetGuildId of realmGuildIds) { - if (targetGuildId === newMember.guild.id) continue; - - try { - const targetGuild = client.guilds.cache.get(targetGuildId); - if (!targetGuild) continue; - - const targetMember = await targetGuild.members.fetch(newMember.user.id).catch(() => null); - if (!targetMember) continue; - - const targetRole = targetGuild.roles.cache.find(r => r.name === role.name); - if (!targetRole) continue; - - if (!targetMember.roles.cache.has(targetRole.id)) { - await targetMember.roles.add(targetRole, '[Federation] Cross-server sync'); - console.log(`[Federation] Added ${role.name} to ${newMember.user.tag} in ${targetGuild.name}`); - } - } catch (err) { - console.error(`[Federation] Sync error for guild ${targetGuildId}:`, err.message); - } - } - } - - for (const [roleId, role] of removedRoles) { - const mapping = federationMappings.get(roleId); - if (!mapping) continue; - - console.log(`[Federation] Removing synced role ${role.name} for ${newMember.user.tag} across realms`); - - for (const targetGuildId of realmGuildIds) { - if (targetGuildId === newMember.guild.id) continue; - - try { - const targetGuild = client.guilds.cache.get(targetGuildId); - if (!targetGuild) continue; - - const targetMember = await targetGuild.members.fetch(newMember.user.id).catch(() => null); - if (!targetMember) continue; - - const targetRole = targetGuild.roles.cache.find(r => r.name === role.name); - if (!targetRole) continue; - - if (targetMember.roles.cache.has(targetRole.id)) { - await targetMember.roles.remove(targetRole, '[Federation] Cross-server sync'); - console.log(`[Federation] Removed ${role.name} from ${newMember.user.tag} in ${targetGuild.name}`); - } - } catch (err) { - console.error(`[Federation] Sync error for guild ${targetGuildId}:`, err.message); - } - } - } - } catch (error) { - console.error('[Federation] guildMemberUpdate error:', error); - } - }, -}; diff --git a/aethex-bot/events/messageCreate.js b/aethex-bot/events/messageCreate.js new file mode 100644 index 0000000..afc0b30 --- /dev/null +++ b/aethex-bot/events/messageCreate.js @@ -0,0 +1,181 @@ +const { createClient } = require("@supabase/supabase-js"); + +let supabase = null; +if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) { + 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) { + if (!supabase) return; + + if (message.author.bot) return; + + if (!message.content && message.attachments.size === 0) return; + + if (!FEED_CHANNEL_ID) { + return; + } + + if (message.channelId !== FEED_CHANNEL_ID) { + return; + } + + await syncMessageToFeed(message); + }, +}; diff --git a/aethex-bot/listeners/feedSync.js b/aethex-bot/listeners/feedSync.js new file mode 100644 index 0000000..b8168c4 --- /dev/null +++ b/aethex-bot/listeners/feedSync.js @@ -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 }; diff --git a/aethex-bot/listeners/sentinel/antiNuke.js b/aethex-bot/listeners/sentinel/antiNuke.js index 161a935..0c0138d 100644 --- a/aethex-bot/listeners/sentinel/antiNuke.js +++ b/aethex-bot/listeners/sentinel/antiNuke.js @@ -1,55 +1,47 @@ -const { EmbedBuilder, AuditLogEvent } = require('discord.js'); +const { EmbedBuilder } = require("discord.js"); module.exports = { - name: 'channelDelete', + name: "channelDelete", async execute(channel, client) { - try { - const guild = channel.guild; - if (!guild) return; - - const auditLogs = await guild.fetchAuditLogs({ - type: AuditLogEvent.ChannelDelete, - limit: 1, - }); - - const log = auditLogs.entries.first(); - if (!log) return; - - const { executor, target } = log; - if (!executor || executor.id === client.user.id) return; - - const heat = client.addHeat(executor.id, 'CHANNEL_DELETE'); - console.log(`[Sentinel] User ${executor.tag} deleted channel. Heat: ${heat}/${client.HEAT_THRESHOLD}`); - - if (heat >= client.HEAT_THRESHOLD) { - const embed = new EmbedBuilder() - .setColor(0xff0000) - .setTitle('ANTI-NUKE TRIGGERED') - .setDescription(`User **${executor.tag}** has been banned for deleting too many channels.`) - .addFields( - { name: 'User ID', value: executor.id, inline: true }, - { name: 'Heat Level', value: `${heat}/${client.HEAT_THRESHOLD}`, inline: true }, - { name: 'Action', value: 'Automatic Ban', inline: true } - ) - .setTimestamp(); - - try { - await guild.members.ban(executor.id, { reason: '[Sentinel] Anti-nuke: Mass channel deletion detected' }); - console.log(`[Sentinel] BANNED ${executor.tag} for mass channel deletion`); - - client.sendAlert(`ANTI-NUKE: Banned ${executor.tag} for mass channel deletion`, embed); - - const owner = await guild.fetchOwner(); - if (owner) { - await owner.send({ content: `[SENTINEL ALERT] User ${executor.tag} was banned for mass channel deletion in ${guild.name}`, embeds: [embed] }).catch(() => {}); - } - } catch (banError) { - console.error('[Sentinel] Failed to ban user:', banError.message); - client.sendAlert(`ANTI-NUKE FAILED: Could not ban ${executor.tag} - ${banError.message}`); - } + if (!channel.guild) return; + + const auditLogs = await channel.guild.fetchAuditLogs({ + type: 12, + limit: 1, + }).catch(() => null); + + if (!auditLogs || !auditLogs.entries.first()) return; + + const entry = auditLogs.entries.first(); + const executor = entry.executor; + + if (!executor || executor.bot) return; + if (executor.id === channel.guild.ownerId) return; + + const heatLevel = client.addHeat(executor.id, "channel_delete"); + console.log(`[Sentinel] ${executor.tag} deleted channel #${channel.name} - Heat: ${heatLevel}/${client.HEAT_THRESHOLD}`); + + if (heatLevel >= client.HEAT_THRESHOLD) { + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("SENTINEL ALERT: Mass Channel Deletion Detected") + .setDescription(`**User:** ${executor.tag} (${executor.id})\n**Action:** Deleted ${heatLevel} channels in rapid succession\n**Server:** ${channel.guild.name}`) + .setTimestamp(); + + try { + const member = await channel.guild.members.fetch(executor.id); + await member.ban({ reason: "[Sentinel] Anti-nuke triggered: Mass channel deletion" }); + embed.addFields({ name: "Action Taken", value: "User has been banned" }); + } catch (err) { + embed.addFields({ name: "Action Failed", value: `Could not ban user: ${err.message}` }); } - } catch (error) { - console.error('[Sentinel] Channel delete handler error:', error); + + client.sendAlert(`SENTINEL ALERT`, embed); + + try { + const owner = await channel.guild.fetchOwner(); + await owner.send({ content: `[Sentinel Alert] ${executor.tag} was detected deleting multiple channels and has been banned from ${channel.guild.name}.`, embeds: [embed] }); + } catch (e) {} } }, }; diff --git a/aethex-bot/listeners/sentinel/memberBan.js b/aethex-bot/listeners/sentinel/memberBan.js index c847d55..ae67c15 100644 --- a/aethex-bot/listeners/sentinel/memberBan.js +++ b/aethex-bot/listeners/sentinel/memberBan.js @@ -1,53 +1,48 @@ -const { EmbedBuilder, AuditLogEvent } = require('discord.js'); +const { EmbedBuilder } = require("discord.js"); module.exports = { - name: 'guildBanAdd', + name: "guildBanAdd", async execute(ban, client) { - try { - const guild = ban.guild; - - const auditLogs = await guild.fetchAuditLogs({ - type: AuditLogEvent.MemberBanAdd, - limit: 1, - }); - - const log = auditLogs.entries.first(); - if (!log) return; - - const { executor, target } = log; - if (!executor || executor.id === client.user.id) return; - - const heat = client.addHeat(executor.id, 'MEMBER_BAN_ADD'); - console.log(`[Sentinel] User ${executor.tag} banned ${target.tag}. Heat: ${heat}/${client.HEAT_THRESHOLD}`); - - if (heat >= client.HEAT_THRESHOLD) { - const embed = new EmbedBuilder() - .setColor(0xff0000) - .setTitle('ANTI-NUKE TRIGGERED') - .setDescription(`User **${executor.tag}** has been banned for mass banning members.`) - .addFields( - { name: 'User ID', value: executor.id, inline: true }, - { name: 'Heat Level', value: `${heat}/${client.HEAT_THRESHOLD}`, inline: true }, - { name: 'Action', value: 'Mass Ban Detected', inline: true } - ) - .setTimestamp(); - - try { - await guild.members.ban(executor.id, { reason: '[Sentinel] Anti-nuke: Mass banning detected' }); - console.log(`[Sentinel] BANNED ${executor.tag} for mass banning`); - - client.sendAlert(`ANTI-NUKE: Banned ${executor.tag} for mass banning`, embed); - - const owner = await guild.fetchOwner(); - if (owner) { - await owner.send({ content: `[SENTINEL ALERT] User ${executor.tag} was banned for mass banning in ${guild.name}`, embeds: [embed] }).catch(() => {}); - } - } catch (banError) { - console.error('[Sentinel] Failed to ban user:', banError.message); - } + const guild = ban.guild; + + const auditLogs = await guild.fetchAuditLogs({ + type: 22, + limit: 1, + }).catch(() => null); + + if (!auditLogs || !auditLogs.entries.first()) return; + + const entry = auditLogs.entries.first(); + const executor = entry.executor; + + if (!executor || executor.bot) return; + if (executor.id === guild.ownerId) return; + if (executor.id === client.user.id) return; + + const heatLevel = client.addHeat(executor.id, "member_ban"); + console.log(`[Sentinel] ${executor.tag} banned ${ban.user.tag} - Heat: ${heatLevel}/${client.HEAT_THRESHOLD}`); + + if (heatLevel >= client.HEAT_THRESHOLD) { + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("SENTINEL ALERT: Mass Ban Detected") + .setDescription(`**User:** ${executor.tag} (${executor.id})\n**Action:** Banned ${heatLevel} members in rapid succession\n**Server:** ${guild.name}`) + .setTimestamp(); + + try { + const member = await guild.members.fetch(executor.id); + await member.ban({ reason: "[Sentinel] Anti-nuke triggered: Mass banning" }); + embed.addFields({ name: "Action Taken", value: "User has been banned" }); + } catch (err) { + embed.addFields({ name: "Action Failed", value: `Could not ban user: ${err.message}` }); } - } catch (error) { - console.error('[Sentinel] Member ban handler error:', error); + + client.sendAlert(`SENTINEL ALERT`, embed); + + try { + const owner = await guild.fetchOwner(); + await owner.send({ content: `[Sentinel Alert] ${executor.tag} was detected mass banning members and has been banned from ${guild.name}.`, embeds: [embed] }); + } catch (e) {} } }, }; diff --git a/aethex-bot/listeners/sentinel/memberKick.js b/aethex-bot/listeners/sentinel/memberKick.js index c1fd69c..847f0c7 100644 --- a/aethex-bot/listeners/sentinel/memberKick.js +++ b/aethex-bot/listeners/sentinel/memberKick.js @@ -1,56 +1,51 @@ -const { EmbedBuilder, AuditLogEvent } = require('discord.js'); +const { EmbedBuilder } = require("discord.js"); module.exports = { - name: 'guildMemberRemove', + name: "guildMemberRemove", async execute(member, client) { - try { - const guild = member.guild; - - const auditLogs = await guild.fetchAuditLogs({ - type: AuditLogEvent.MemberKick, - limit: 1, - }); - - const log = auditLogs.entries.first(); - if (!log) return; - - const { executor, target, createdTimestamp } = log; - if (!executor || executor.id === client.user.id) return; - - if (Date.now() - createdTimestamp > 5000) return; - if (target.id !== member.id) return; - - const heat = client.addHeat(executor.id, 'MEMBER_KICK'); - console.log(`[Sentinel] User ${executor.tag} kicked ${member.user.tag}. Heat: ${heat}/${client.HEAT_THRESHOLD}`); - - if (heat >= client.HEAT_THRESHOLD) { - const embed = new EmbedBuilder() - .setColor(0xff0000) - .setTitle('ANTI-NUKE TRIGGERED') - .setDescription(`User **${executor.tag}** has been banned for mass kicking members.`) - .addFields( - { name: 'User ID', value: executor.id, inline: true }, - { name: 'Heat Level', value: `${heat}/${client.HEAT_THRESHOLD}`, inline: true }, - { name: 'Action', value: 'Mass Kick Detected', inline: true } - ) - .setTimestamp(); - - try { - await guild.members.ban(executor.id, { reason: '[Sentinel] Anti-nuke: Mass kicking detected' }); - console.log(`[Sentinel] BANNED ${executor.tag} for mass kicking`); - - client.sendAlert(`ANTI-NUKE: Banned ${executor.tag} for mass kicking`, embed); - - const owner = await guild.fetchOwner(); - if (owner) { - await owner.send({ content: `[SENTINEL ALERT] User ${executor.tag} was banned for mass kicking in ${guild.name}`, embeds: [embed] }).catch(() => {}); - } - } catch (banError) { - console.error('[Sentinel] Failed to ban user:', banError.message); - } + const guild = member.guild; + + const auditLogs = await guild.fetchAuditLogs({ + type: 20, + limit: 1, + }).catch(() => null); + + if (!auditLogs || !auditLogs.entries.first()) return; + + const entry = auditLogs.entries.first(); + + if (Date.now() - entry.createdTimestamp > 5000) return; + if (entry.target.id !== member.id) return; + + const executor = entry.executor; + + if (!executor || executor.bot) return; + if (executor.id === guild.ownerId) return; + + const heatLevel = client.addHeat(executor.id, "member_kick"); + console.log(`[Sentinel] ${executor.tag} kicked ${member.user.tag} - Heat: ${heatLevel}/${client.HEAT_THRESHOLD}`); + + if (heatLevel >= client.HEAT_THRESHOLD) { + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("SENTINEL ALERT: Mass Kick Detected") + .setDescription(`**User:** ${executor.tag} (${executor.id})\n**Action:** Kicked ${heatLevel} members in rapid succession\n**Server:** ${guild.name}`) + .setTimestamp(); + + try { + const kickerMember = await guild.members.fetch(executor.id); + await kickerMember.ban({ reason: "[Sentinel] Anti-nuke triggered: Mass kicking" }); + embed.addFields({ name: "Action Taken", value: "User has been banned" }); + } catch (err) { + embed.addFields({ name: "Action Failed", value: `Could not ban user: ${err.message}` }); } - } catch (error) { - console.error('[Sentinel] Member kick handler error:', error); + + client.sendAlert(`SENTINEL ALERT`, embed); + + try { + const owner = await guild.fetchOwner(); + await owner.send({ content: `[Sentinel Alert] ${executor.tag} was detected mass kicking members and has been banned from ${guild.name}.`, embeds: [embed] }); + } catch (e) {} } }, }; diff --git a/aethex-bot/listeners/sentinel/roleDelete.js b/aethex-bot/listeners/sentinel/roleDelete.js index 3fe5690..2dcb751 100644 --- a/aethex-bot/listeners/sentinel/roleDelete.js +++ b/aethex-bot/listeners/sentinel/roleDelete.js @@ -1,54 +1,47 @@ -const { EmbedBuilder, AuditLogEvent } = require('discord.js'); +const { EmbedBuilder } = require("discord.js"); module.exports = { - name: 'roleDelete', + name: "roleDelete", async execute(role, client) { - try { - const guild = role.guild; - if (!guild) return; - - const auditLogs = await guild.fetchAuditLogs({ - type: AuditLogEvent.RoleDelete, - limit: 1, - }); - - const log = auditLogs.entries.first(); - if (!log) return; - - const { executor } = log; - if (!executor || executor.id === client.user.id) return; - - const heat = client.addHeat(executor.id, 'ROLE_DELETE'); - console.log(`[Sentinel] User ${executor.tag} deleted role ${role.name}. Heat: ${heat}/${client.HEAT_THRESHOLD}`); - - if (heat >= client.HEAT_THRESHOLD) { - const embed = new EmbedBuilder() - .setColor(0xff0000) - .setTitle('ANTI-NUKE TRIGGERED') - .setDescription(`User **${executor.tag}** has been banned for deleting too many roles.`) - .addFields( - { name: 'User ID', value: executor.id, inline: true }, - { name: 'Heat Level', value: `${heat}/${client.HEAT_THRESHOLD}`, inline: true }, - { name: 'Deleted Role', value: role.name, inline: true } - ) - .setTimestamp(); - - try { - await guild.members.ban(executor.id, { reason: '[Sentinel] Anti-nuke: Mass role deletion detected' }); - console.log(`[Sentinel] BANNED ${executor.tag} for mass role deletion`); - - client.sendAlert(`ANTI-NUKE: Banned ${executor.tag} for mass role deletion`, embed); - - const owner = await guild.fetchOwner(); - if (owner) { - await owner.send({ content: `[SENTINEL ALERT] User ${executor.tag} was banned for mass role deletion in ${guild.name}`, embeds: [embed] }).catch(() => {}); - } - } catch (banError) { - console.error('[Sentinel] Failed to ban user:', banError.message); - } + if (!role.guild) return; + + const auditLogs = await role.guild.fetchAuditLogs({ + type: 32, + limit: 1, + }).catch(() => null); + + if (!auditLogs || !auditLogs.entries.first()) return; + + const entry = auditLogs.entries.first(); + const executor = entry.executor; + + if (!executor || executor.bot) return; + if (executor.id === role.guild.ownerId) return; + + const heatLevel = client.addHeat(executor.id, "role_delete"); + console.log(`[Sentinel] ${executor.tag} deleted role @${role.name} - Heat: ${heatLevel}/${client.HEAT_THRESHOLD}`); + + if (heatLevel >= client.HEAT_THRESHOLD) { + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("SENTINEL ALERT: Mass Role Deletion Detected") + .setDescription(`**User:** ${executor.tag} (${executor.id})\n**Action:** Deleted ${heatLevel} roles in rapid succession\n**Server:** ${role.guild.name}`) + .setTimestamp(); + + try { + const member = await role.guild.members.fetch(executor.id); + await member.ban({ reason: "[Sentinel] Anti-nuke triggered: Mass role deletion" }); + embed.addFields({ name: "Action Taken", value: "User has been banned" }); + } catch (err) { + embed.addFields({ name: "Action Failed", value: `Could not ban user: ${err.message}` }); } - } catch (error) { - console.error('[Sentinel] Role delete handler error:', error); + + client.sendAlert(`SENTINEL ALERT`, embed); + + try { + const owner = await role.guild.fetchOwner(); + await owner.send({ content: `[Sentinel Alert] ${executor.tag} was detected deleting multiple roles and has been banned from ${role.guild.name}.`, embeds: [embed] }); + } catch (e) {} } }, }; diff --git a/aethex-bot/scripts/register-commands.js b/aethex-bot/scripts/register-commands.js index 8c84277..27ffb8d 100644 --- a/aethex-bot/scripts/register-commands.js +++ b/aethex-bot/scripts/register-commands.js @@ -1,89 +1,110 @@ -const { REST, Routes } = require('discord.js'); -const fs = require('fs'); -const path = require('path'); -require('dotenv').config(); +const { REST, Routes } = require("discord.js"); +const fs = require("fs"); +const path = require("path"); +require("dotenv").config(); -const token = process.env.DISCORD_BOT_TOKEN; -const clientId = process.env.DISCORD_CLIENT_ID; +// Validate environment variables +const requiredEnvVars = ["DISCORD_BOT_TOKEN", "DISCORD_CLIENT_ID"]; -if (!token) { - console.error('Missing DISCORD_BOT_TOKEN'); +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); } -if (!clientId) { - console.error('Missing DISCORD_CLIENT_ID'); - process.exit(1); -} - -const commandsPath = path.join(__dirname, '../commands'); -const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); +// 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) { + if ("data" in command && "execute" in command) { commands.push(command.data.toJSON()); - console.log(`Loaded command: ${command.data.name}`); + console.log(`✅ Loaded command: ${command.data.name}`); } } -const rest = new REST({ version: '10' }).setToken(token); - -(async () => { +// Register commands with Discord API +async function registerCommands() { try { - console.log(`\nFetching existing commands...`); - - const existingCommands = await rest.get( - Routes.applicationCommands(clientId) + const rest = new REST({ version: "10" }).setToken( + process.env.DISCORD_BOT_TOKEN, ); - - console.log(`Found ${existingCommands.length} existing commands`); - - const entryPointCommands = existingCommands.filter(cmd => cmd.type === 4); - - const allCommands = [...commands, ...entryPointCommands.map(cmd => ({ - name: cmd.name, - description: cmd.description, - type: cmd.type, - handler: cmd.handler, - }))]; - - console.log(`Registering ${commands.length} commands (preserving ${entryPointCommands.length} entry points)...`); - - const data = await rest.put( - Routes.applicationCommands(clientId), - { body: allCommands } + + console.log(`\n📝 Registering ${commands.length} slash commands...`); + console.log( + "⚠️ This will co-exist with Discord's auto-generated Entry Point command.\n", ); - - console.log(`\nSuccessfully registered ${data.length} commands:`); - data.forEach(cmd => console.log(` - /${cmd.name}`)); - } catch (error) { - if (error.code === 50240) { - console.warn('\nEntry Point detected. Registering individually...'); - - let successCount = 0; - for (const command of commands) { - try { - await rest.post( - Routes.applicationCommands(clientId), - { body: command } - ); - console.log(` + /${command.name}`); - successCount++; - } catch (postError) { - if (postError.code === 50045) { - console.log(` ~ /${command.name} (already exists)`); - } else { - console.error(` x /${command.name}: ${postError.message}`); + + 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; } - console.log(`\nRegistered ${successCount} commands individually.`); - } else { - console.error('Error registering commands:', error); } + } catch (error) { + console.error( + "❌ Fatal error registering commands:", + error.message || error, + ); + process.exit(1); } -})(); +} + +// Run registration +registerCommands(); diff --git a/attached_assets/discord-bot-source/discord-bot/.env.example b/attached_assets/discord-bot-source/discord-bot/.env.example new file mode 100644 index 0000000..a4ddc91 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/.env.example @@ -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 diff --git a/attached_assets/discord-bot-source/discord-bot/DEPLOYMENT_GUIDE.md b/attached_assets/discord-bot-source/discord-bot/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..c90a3aa --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/DEPLOYMENT_GUIDE.md @@ -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= +DISCORD_CLIENT_ID= +DISCORD_PUBLIC_KEY= +SUPABASE_URL= +SUPABASE_SERVICE_ROLE= +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 # + 📡 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:/// +``` + +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:** `` +**Bot Status:** `` +**Last Updated:** `` diff --git a/attached_assets/discord-bot-source/discord-bot/Dockerfile b/attached_assets/discord-bot-source/discord-bot/Dockerfile new file mode 100644 index 0000000..279b52f --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/Dockerfile @@ -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"] diff --git a/attached_assets/discord-bot-source/discord-bot/bot.js b/attached_assets/discord-bot-source/discord-bot/bot.js new file mode 100644 index 0000000..401b132 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/bot.js @@ -0,0 +1,803 @@ +const { + Client, + GatewayIntentBits, + REST, + Routes, + Collection, + EmbedBuilder, +} = require("discord.js"); +const { createClient } = require("@supabase/supabase-js"); +const http = require("http"); +const fs = require("fs"); +const path = require("path"); +require("dotenv").config(); + +const { setupFeedListener, sendPostToDiscord, getFeedChannelId } = require("./listeners/feedSync"); + +// Validate environment variables +const requiredEnvVars = [ + "DISCORD_BOT_TOKEN", + "DISCORD_CLIENT_ID", + "SUPABASE_URL", + "SUPABASE_SERVICE_ROLE", +]; + +const missingVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); +if (missingVars.length > 0) { + console.error( + "❌ FATAL ERROR: Missing required environment variables:", + missingVars.join(", "), + ); + console.error("\nPlease set these in your Discloud/hosting environment:"); + missingVars.forEach((envVar) => { + console.error(` - ${envVar}`); + }); + process.exit(1); +} + +// Validate token format +const token = process.env.DISCORD_BOT_TOKEN; +if (!token || token.length < 20) { + console.error("❌ FATAL ERROR: DISCORD_BOT_TOKEN is empty or invalid"); + console.error(` Length: ${token ? token.length : 0}`); + process.exit(1); +} + +console.log("[Token] Bot token loaded (length: " + token.length + " chars)"); + +// Initialize Discord client with message intents for feed sync +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.DirectMessages, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], +}); + +// Initialize Supabase +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE, +); + +// Store slash commands +client.commands = new Collection(); + +// Load commands from commands directory +const commandsPath = path.join(__dirname, "commands"); +const commandFiles = fs + .readdirSync(commandsPath) + .filter((file) => file.endsWith(".js")); + +for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + const command = require(filePath); + if ("data" in command && "execute" in command) { + client.commands.set(command.data.name, command); + console.log(`✅ Loaded command: ${command.data.name}`); + } +} + +// Load event handlers from events directory +const eventsPath = path.join(__dirname, "events"); +if (fs.existsSync(eventsPath)) { + const eventFiles = fs + .readdirSync(eventsPath) + .filter((file) => file.endsWith(".js")); + + for (const file of eventFiles) { + const filePath = path.join(eventsPath, file); + const event = require(filePath); + if ("name" in event && "execute" in event) { + client.on(event.name, (...args) => + event.execute(...args, client, supabase), + ); + console.log(`✅ Loaded event listener: ${event.name}`); + } + } +} + +// Slash command interaction handler +client.on("interactionCreate", async (interaction) => { + if (!interaction.isChatInputCommand()) return; + + const command = client.commands.get(interaction.commandName); + + if (!command) { + console.warn( + `⚠️ No command matching ${interaction.commandName} was found.`, + ); + return; + } + + try { + await command.execute(interaction, supabase, client); + } catch (error) { + console.error(`❌ Error executing ${interaction.commandName}:`, error); + + const errorEmbed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle("❌ Command Error") + .setDescription("There was an error while executing this command.") + .setFooter({ text: "Contact support if this persists" }); + + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ embeds: [errorEmbed], ephemeral: true }); + } else { + await interaction.reply({ embeds: [errorEmbed], ephemeral: true }); + } + } +}); + +// IMPORTANT: Commands are now registered via a separate script +// Run this ONCE during deployment: npm run register-commands +// This prevents Error 50240 (Entry Point conflict) when Activities are enabled +// The bot will simply load and listen for the already-registered commands + +// Define all commands for registration +const COMMANDS_TO_REGISTER = [ + { + name: "verify", + description: "Link your Discord account to AeThex", + }, + { + name: "set-realm", + description: "Choose your primary arm/realm (Labs, GameForge, Corp, etc.)", + options: [ + { + name: "realm", + type: 3, + description: "Your primary realm", + required: true, + choices: [ + { name: "Labs", value: "labs" }, + { name: "GameForge", value: "gameforge" }, + { name: "Corp", value: "corp" }, + { name: "Foundation", value: "foundation" }, + { name: "Dev-Link", value: "devlink" }, + ], + }, + ], + }, + { + name: "profile", + description: "View your linked AeThex profile", + }, + { + name: "unlink", + description: "Disconnect your Discord account from AeThex", + }, + { + name: "verify-role", + description: "Check your assigned Discord roles", + }, + { + name: "help", + description: "View all AeThex bot commands and features", + }, + { + name: "stats", + description: "View your AeThex statistics and activity", + }, + { + name: "leaderboard", + description: "View the top AeThex contributors", + options: [ + { + name: "category", + type: 3, + description: "Leaderboard category", + required: false, + choices: [ + { name: "Most Active (Posts)", value: "posts" }, + { name: "Most Liked", value: "likes" }, + { name: "Top Creators", value: "creators" }, + ], + }, + ], + }, + { + name: "post", + description: "Create a post in the AeThex community feed", + options: [ + { + name: "content", + type: 3, + description: "Your post content", + required: true, + max_length: 500, + }, + { + name: "category", + type: 3, + description: "Post category", + required: false, + choices: [ + { name: "General", value: "general" }, + { name: "Project Update", value: "project_update" }, + { name: "Question", value: "question" }, + { name: "Idea", value: "idea" }, + { name: "Announcement", value: "announcement" }, + ], + }, + { + name: "image", + type: 11, + description: "Attach an image to your post", + required: false, + }, + ], + }, +]; + +// Function to register commands with Discord +async function registerDiscordCommands() { + try { + const rest = new REST({ version: "10" }).setToken( + process.env.DISCORD_BOT_TOKEN, + ); + + console.log( + `📝 Registering ${COMMANDS_TO_REGISTER.length} slash commands...`, + ); + + try { + // Try bulk update first + const data = await rest.put( + Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), + { body: COMMANDS_TO_REGISTER }, + ); + + console.log(`✅ Successfully registered ${data.length} slash commands`); + return { success: true, count: data.length, results: null }; + } catch (bulkError) { + // Handle Error 50240 (Entry Point conflict) + if (bulkError.code === 50240) { + console.warn( + "⚠️ Error 50240: Entry Point detected. Registering individually...", + ); + + const results = []; + let successCount = 0; + let skipCount = 0; + + for (const command of COMMANDS_TO_REGISTER) { + try { + const posted = await rest.post( + Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), + { body: command }, + ); + results.push({ + name: command.name, + status: "registered", + id: posted.id, + }); + successCount++; + } catch (postError) { + if (postError.code === 50045) { + results.push({ + name: command.name, + status: "already_exists", + }); + skipCount++; + } else { + results.push({ + name: command.name, + status: "error", + error: postError.message, + }); + } + } + } + + console.log( + `✅ Registration complete: ${successCount} new, ${skipCount} already existed`, + ); + return { + success: true, + count: successCount, + skipped: skipCount, + results, + }; + } + + throw bulkError; + } + } catch (error) { + console.error("❌ Failed to register commands:", error); + return { success: false, error: error.message }; + } +} + +// Start HTTP health check server +const healthPort = process.env.HEALTH_PORT || 8080; +const ADMIN_TOKEN = process.env.DISCORD_ADMIN_TOKEN || "aethex-bot-admin"; + +// Helper to check admin authentication +const checkAdminAuth = (req) => { + const authHeader = req.headers.authorization; + return authHeader === `Bearer ${ADMIN_TOKEN}`; +}; + +http + .createServer((req, res) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + res.setHeader("Content-Type", "application/json"); + + if (req.method === "OPTIONS") { + res.writeHead(200); + res.end(); + return; + } + + if (req.url === "/health") { + res.writeHead(200); + res.end( + JSON.stringify({ + status: "online", + guilds: client.guilds.cache.size, + commands: client.commands.size, + uptime: Math.floor(process.uptime()), + timestamp: new Date().toISOString(), + }), + ); + return; + } + + // GET /bot-status - Comprehensive bot status for management panel (requires auth) + if (req.url === "/bot-status") { + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + return; + } + + const channelId = getFeedChannelId(); + const guilds = client.guilds.cache.map((guild) => ({ + id: guild.id, + name: guild.name, + memberCount: guild.memberCount, + icon: guild.iconURL(), + })); + + res.writeHead(200); + res.end( + JSON.stringify({ + status: client.isReady() ? "online" : "offline", + bot: { + tag: client.user?.tag || "Not logged in", + id: client.user?.id, + avatar: client.user?.displayAvatarURL(), + }, + guilds: guilds, + guildCount: client.guilds.cache.size, + commands: Array.from(client.commands.keys()), + commandCount: client.commands.size, + uptime: Math.floor(process.uptime()), + feedBridge: { + enabled: !!channelId, + channelId: channelId, + }, + timestamp: new Date().toISOString(), + }), + ); + return; + } + + // GET /linked-users - Get all Discord-linked users (requires auth, sanitizes PII) + if (req.url === "/linked-users") { + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + return; + } + + (async () => { + try { + const { data: links, error } = await supabase + .from("discord_links") + .select("discord_id, user_id, primary_arm, created_at") + .order("created_at", { ascending: false }) + .limit(50); + + if (error) throw error; + + const enrichedLinks = await Promise.all( + (links || []).map(async (link) => { + const { data: profile } = await supabase + .from("user_profiles") + .select("username, avatar_url") + .eq("id", link.user_id) + .single(); + + return { + discord_id: link.discord_id.slice(0, 6) + "***", + user_id: link.user_id.slice(0, 8) + "...", + primary_arm: link.primary_arm, + created_at: link.created_at, + profile: profile ? { + username: profile.username, + avatar_url: profile.avatar_url, + } : null, + }; + }) + ); + + res.writeHead(200); + res.end(JSON.stringify({ success: true, links: enrichedLinks, count: enrichedLinks.length })); + } catch (error) { + res.writeHead(500); + res.end(JSON.stringify({ success: false, error: error.message })); + } + })(); + return; + } + + // GET /command-stats - Get command usage statistics (requires auth) + if (req.url === "/command-stats") { + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + return; + } + + (async () => { + try { + const stats = { + commands: COMMANDS_TO_REGISTER.map((cmd) => ({ + name: cmd.name, + description: cmd.description, + options: cmd.options?.length || 0, + })), + totalCommands: COMMANDS_TO_REGISTER.length, + }; + + res.writeHead(200); + res.end(JSON.stringify({ success: true, stats })); + } catch (error) { + res.writeHead(500); + res.end(JSON.stringify({ success: false, error: error.message })); + } + })(); + return; + } + + // GET /feed-stats - Get feed bridge statistics (requires auth) + if (req.url === "/feed-stats") { + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + return; + } + + (async () => { + try { + const { count: totalPosts } = await supabase + .from("community_posts") + .select("*", { count: "exact", head: true }); + + const { count: discordPosts } = await supabase + .from("community_posts") + .select("*", { count: "exact", head: true }) + .eq("source", "discord"); + + const { count: websitePosts } = await supabase + .from("community_posts") + .select("*", { count: "exact", head: true }) + .or("source.is.null,source.neq.discord"); + + const { data: recentPosts } = await supabase + .from("community_posts") + .select("id, content, source, created_at") + .order("created_at", { ascending: false }) + .limit(10); + + res.writeHead(200); + res.end( + JSON.stringify({ + success: true, + stats: { + totalPosts: totalPosts || 0, + discordPosts: discordPosts || 0, + websitePosts: websitePosts || 0, + recentPosts: (recentPosts || []).map(p => ({ + id: p.id, + content: p.content?.slice(0, 100) + (p.content?.length > 100 ? "..." : ""), + source: p.source, + created_at: p.created_at, + })), + }, + }) + ); + } catch (error) { + res.writeHead(500); + res.end(JSON.stringify({ success: false, error: error.message })); + } + })(); + return; + } + + // POST /send-to-discord - Send a post from AeThex to Discord channel + if (req.url === "/send-to-discord" && req.method === "POST") { + let body = ""; + req.on("data", (chunk) => { + body += chunk.toString(); + }); + req.on("end", async () => { + try { + // Simple auth check + const authHeader = req.headers.authorization; + const expectedToken = process.env.DISCORD_BRIDGE_TOKEN || "aethex-bridge"; + if (authHeader !== `Bearer ${expectedToken}`) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized" })); + return; + } + + const post = JSON.parse(body); + console.log("[API] Received post to send to Discord:", post.id); + + const result = await sendPostToDiscord(post, post.author); + res.writeHead(result.success ? 200 : 500); + res.end(JSON.stringify(result)); + } catch (error) { + console.error("[API] Error processing send-to-discord:", error); + res.writeHead(500); + res.end(JSON.stringify({ error: error.message })); + } + }); + return; + } + + // GET /bridge-status - Check if bridge is configured + if (req.url === "/bridge-status") { + const channelId = getFeedChannelId(); + res.writeHead(200); + res.end( + JSON.stringify({ + enabled: !!channelId, + channelId: channelId, + botReady: client.isReady(), + }), + ); + return; + } + + if (req.url === "/register-commands") { + if (req.method === "GET") { + if (!checkAdminAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized - Admin token required" })); + return; + } + // Show HTML form with button + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(` + + + + Register Discord Commands + + + +
+

🤖 Discord Commands Registration

+

Click the button below to register all Discord slash commands (/verify, /set-realm, /profile, /unlink, /verify-role)

+ + + +
⏳ Registering... please wait...
+
+
+ + + + + `); + 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(`���� 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; diff --git a/attached_assets/discord-bot-source/discord-bot/commands/help.js b/attached_assets/discord-bot-source/discord-bot/commands/help.js new file mode 100644 index 0000000..324b1dd --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/commands/leaderboard.js b/attached_assets/discord-bot-source/discord-bot/commands/leaderboard.js new file mode 100644 index 0000000..cbc5b01 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/commands/post.js b/attached_assets/discord-bot-source/discord-bot/commands/post.js new file mode 100644 index 0000000..61057e6 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/commands/profile.js b/attached_assets/discord-bot-source/discord-bot/commands/profile.js new file mode 100644 index 0000000..035f251 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/commands/refresh-roles.js b/attached_assets/discord-bot-source/discord-bot/commands/refresh-roles.js new file mode 100644 index 0000000..459bd79 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/commands/set-realm.js b/attached_assets/discord-bot-source/discord-bot/commands/set-realm.js new file mode 100644 index 0000000..c1af120 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/commands/stats.js b/attached_assets/discord-bot-source/discord-bot/commands/stats.js new file mode 100644 index 0000000..fe9814b --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/commands/unlink.js b/attached_assets/discord-bot-source/discord-bot/commands/unlink.js new file mode 100644 index 0000000..ac06d2a --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/commands/verify-role.js b/attached_assets/discord-bot-source/discord-bot/commands/verify-role.js new file mode 100644 index 0000000..1b7e6b9 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/commands/verify.js b/attached_assets/discord-bot-source/discord-bot/commands/verify.js new file mode 100644 index 0000000..d9f30e7 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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/attached_assets/discord-bot-source/discord-bot/discloud.config b/attached_assets/discord-bot-source/discord-bot/discloud.config new file mode 100644 index 0000000..fe114e6 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/discloud.config @@ -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 diff --git a/attached_assets/discord-bot-source/discord-bot/events/messageCreate.js b/attached_assets/discord-bot-source/discord-bot/events/messageCreate.js new file mode 100644 index 0000000..75abc33 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/events/messageCreate.js @@ -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); + }, +}; diff --git a/attached_assets/discord-bot-source/discord-bot/listeners/feedSync.js b/attached_assets/discord-bot-source/discord-bot/listeners/feedSync.js new file mode 100644 index 0000000..b8168c4 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/listeners/feedSync.js @@ -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 }; diff --git a/attached_assets/discord-bot-source/discord-bot/package-lock.json b/attached_assets/discord-bot-source/discord-bot/package-lock.json new file mode 100644 index 0000000..15b33b8 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/package-lock.json @@ -0,0 +1,1157 @@ +{ + "name": "aethex-discord-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aethex-discord-bot", + "version": "1.0.0", + "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" + } + }, + "node_modules/@discord/embedded-app-sdk": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@discord/embedded-app-sdk/-/embedded-app-sdk-2.4.0.tgz", + "integrity": "sha512-kIIS79tuVKvu9YC6GIuvBSfUqNa6511UqafD4i3qGjWSRqVulioYuRzZ+M9D9/KZ2wuu0nQ5IWIYlnh1bsy2tg==", + "license": "MIT", + "dependencies": { + "@types/lodash.transform": "^4.6.6", + "@types/uuid": "^10.0.0", + "big-integer": "^1.6.48", + "decimal.js-light": "^2.5.0", + "eventemitter3": "^5.0.0", + "lodash.transform": "^4.6.0", + "uuid": "^11.0.0", + "zod": "^3.9.8" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.0.tgz", + "integrity": "sha512-COK0uU6ZaJI+LA67H/rp8IbEkYwlZf3mAoBI5wtPh5G5cbEQGNhVpzINg2f/6+q/YipnNIKy6fJDg6kMUKUw4Q==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.1", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", + "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.1" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.80.0.tgz", + "integrity": "sha512-q2LyCVJGN4p7d92cOI7scWOoNwxJhZuFRwiimSUGJGI5zX7ubf1WUPznwOmYEn8WVo3Io+MyMinA7era6j5KPw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.80.0.tgz", + "integrity": "sha512-0S/k8LRtoblrbzy4ir9m4WuvU/XTkb1EwL/33/oJexCUHCXtsqaPJ3eKfr1GWtNqTa1zryv6sXs3Fpv7lKCsMQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.80.0.tgz", + "integrity": "sha512-yKzehXlRbDoXIQefdRQnvaI9BEogoWIp/7+y/m5enZDKW2IP9aAgq5tU72sThcwftDJvknnIpEHAABG3qviEng==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.80.0.tgz", + "integrity": "sha512-cXK6Gs4UDylN8oz40omi01QK0cSCBVj0efXC1WodpENTuDnrkUs28W8/eslEnAtlawaVtikC1Q92mpz9+o85Mg==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.80.0.tgz", + "integrity": "sha512-Iepod83h2WoMCaLC9pGb3QOT67Kn3RlUdbXpo3uvbDKfPU8EgytS4RVaPmDjhqDjj8AGaiz9mk/ppd2Q2WS+gw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.80.0.tgz", + "integrity": "sha512-n8pkXQxuo5zCWXX5cbSNZj1vuWS8IVNGWTmP1m31Iq1k0e8lPZ07PF08TRV79HHq3mEPP/Ko//BQuflHvY2o8w==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.80.0", + "@supabase/functions-js": "2.80.0", + "@supabase/postgrest-js": "2.80.0", + "@supabase/realtime-js": "2.80.0", + "@supabase/storage-js": "2.80.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "license": "MIT" + }, + "node_modules/@types/lodash.transform": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.transform/-/lodash.transform-4.6.9.tgz", + "integrity": "sha512-1iIn+l7Vrj8hsr2iZLtxRkcV9AtjTafIyxKO9DX2EEcdOgz3Op5dhwKQFhMJgdfIRbYHBUF+SU97Y6P+zyLXNg==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord.js": { + "version": "14.24.2", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.2.tgz", + "integrity": "sha512-VMEDbmguRdX/EeMaTsf9Mb0IQA90WdYF2cn4QDfslQFXgQ6LFtmlPn0FSotnS0kcFbFp+JBSIxtnF+bnAHG/hQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.1", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/lodash.transform": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz", + "integrity": "sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ==", + "license": "MIT" + }, + "node_modules/magic-bytes.js": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/attached_assets/discord-bot-source/discord-bot/package.json b/attached_assets/discord-bot-source/discord-bot/package.json new file mode 100644 index 0000000..b65f0ac --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/package.json @@ -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" + } +} diff --git a/attached_assets/discord-bot-source/discord-bot/scripts/register-commands.js b/attached_assets/discord-bot-source/discord-bot/scripts/register-commands.js new file mode 100644 index 0000000..27ffb8d --- /dev/null +++ b/attached_assets/discord-bot-source/discord-bot/scripts/register-commands.js @@ -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(); diff --git a/attached_assets/discord-bot-source/discord-bot/utils/roleManager.js b/attached_assets/discord-bot-source/discord-bot/utils/roleManager.js new file mode 100644 index 0000000..27be439 --- /dev/null +++ b/attached_assets/discord-bot-source/discord-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, +}; diff --git a/attached_assets/discord-bot_1765148924805.zip b/attached_assets/discord-bot_1765148924805.zip new file mode 100644 index 0000000..b89da89 Binary files /dev/null and b/attached_assets/discord-bot_1765148924805.zip differ