diff --git a/.replit b/.replit index f9aee22..b7e8f28 100644 --- a/.replit +++ b/.replit @@ -23,7 +23,7 @@ localPort = 8080 externalPort = 8080 [[ports]] -localPort = 33701 +localPort = 37941 externalPort = 3000 [workflows] diff --git a/aethex-bot/bot.js b/aethex-bot/bot.js index e18d28b..f02f448 100644 --- a/aethex-bot/bot.js +++ b/aethex-bot/bot.js @@ -334,6 +334,46 @@ if (fs.existsSync(sentinelPath)) { } } +// ============================================================================= +// GENERAL LISTENER LOADING (Welcome, Goodbye, XP Tracker) +// ============================================================================= + +const listenersPath = path.join(__dirname, "listeners"); +const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js']; +for (const file of generalListenerFiles) { + const filePath = path.join(listenersPath, file); + if (fs.existsSync(filePath)) { + const listener = require(filePath); + if ("name" in listener && "execute" in listener) { + client.on(listener.name, (...args) => listener.execute(...args, client, supabase)); + console.log(`Loaded listener: ${file}`); + } + } +} + +// ============================================================================= +// SERVER CONFIGS MAP +// ============================================================================= + +const serverConfigs = new Map(); +client.serverConfigs = serverConfigs; + +async function loadServerConfigs() { + if (!supabase) return; + try { + const { data, error } = await supabase + .from('server_config') + .select('*'); + if (error) throw error; + for (const config of data || []) { + serverConfigs.set(config.guild_id, config); + } + console.log(`[Config] Loaded ${serverConfigs.size} server configurations`); + } catch (e) { + console.warn('[Config] Could not load server configs:', e.message); + } +} + // ============================================================================= // FEED SYNC SETUP (Modified: Guard for missing Supabase) // ============================================================================= @@ -1020,6 +1060,7 @@ client.once("clientReady", async () => { // Load persisted data from Supabase await loadFederationMappings(); await loadActiveTickets(); + await loadServerConfigs(); // Auto-register commands on startup console.log("Registering slash commands with Discord..."); diff --git a/aethex-bot/commands/avatar.js b/aethex-bot/commands/avatar.js new file mode 100644 index 0000000..af6b0ca --- /dev/null +++ b/aethex-bot/commands/avatar.js @@ -0,0 +1,36 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('avatar') + .setDescription('Get a user\'s avatar') + .addUserOption(option => + option.setName('user') + .setDescription('User to get avatar of (defaults to yourself)') + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user') || interaction.user; + + const sizes = [128, 256, 512, 1024, 2048]; + const links = sizes.map(size => + `[${size}](${target.displayAvatarURL({ size, extension: 'png' })})` + ).join(' | '); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(`${target.tag}'s Avatar`) + .setDescription(`Download: ${links}`) + .setImage(target.displayAvatarURL({ size: 1024, extension: 'png' })) + .setTimestamp(); + + const member = await interaction.guild.members.fetch(target.id).catch(() => null); + if (member && member.avatar) { + embed.setThumbnail(member.displayAvatarURL({ size: 256 })); + embed.setFooter({ text: 'Thumbnail shows server-specific avatar' }); + } + + await interaction.reply({ embeds: [embed] }); + }, +}; diff --git a/aethex-bot/commands/badges.js b/aethex-bot/commands/badges.js new file mode 100644 index 0000000..720b75c --- /dev/null +++ b/aethex-bot/commands/badges.js @@ -0,0 +1,120 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +const BADGE_INFO = { + 'verified': { emoji: 'āœ…', name: 'Verified', description: 'Linked Discord to AeThex' }, + 'early_adopter': { emoji: '🌟', name: 'Early Adopter', description: 'Joined during early access' }, + 'contributor': { emoji: 'šŸ’Ž', name: 'Contributor', description: 'Contributed to the community' }, + 'creator': { emoji: 'šŸŽØ', name: 'Creator', description: 'Published projects on Studio' }, + 'supporter': { emoji: 'ā¤ļø', name: 'Supporter', description: 'Donated to the Foundation' }, + 'level_10': { emoji: 'šŸ”Ÿ', name: 'Level 10', description: 'Reached level 10' }, + 'level_25': { emoji: 'šŸ†', name: 'Level 25', description: 'Reached level 25' }, + 'level_50': { emoji: 'šŸ‘‘', name: 'Level 50', description: 'Reached level 50' }, + 'streak_7': { emoji: 'šŸ”„', name: 'Week Streak', description: '7 day daily claim streak' }, + 'streak_30': { emoji: 'šŸ’Ŗ', name: 'Month Streak', description: '30 day daily claim streak' }, + 'helpful': { emoji: 'šŸ¤', name: 'Helpful', description: 'Helped 10+ community members' }, + 'bug_hunter': { emoji: 'šŸ›', name: 'Bug Hunter', description: 'Reported a valid bug' }, +}; + +module.exports = { + data: new SlashCommandBuilder() + .setName('badges') + .setDescription('View your earned badges across all platforms') + .addUserOption(option => + option.setName('user') + .setDescription('User to view (defaults to yourself)') + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ content: 'Database not configured.', ephemeral: true }); + } + + const target = interaction.options.getUser('user') || interaction.user; + await interaction.deferReply(); + + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id') + .eq('discord_id', target.id) + .single(); + + if (!link) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0xff6b6b) + .setDescription(`${target.id === interaction.user.id ? 'You are' : `${target.tag} is`} not linked to AeThex.`) + ] + }); + } + + const { data: profile } = await supabase + .from('user_profiles') + .select('username, avatar_url, badges, xp, daily_streak') + .eq('id', link.user_id) + .single(); + + let earnedBadges = []; + + if (profile?.badges) { + earnedBadges = typeof profile.badges === 'string' + ? JSON.parse(profile.badges) + : profile.badges; + } + + earnedBadges.push('verified'); + + const xp = profile?.xp || 0; + const level = Math.floor(Math.sqrt(xp / 100)); + if (level >= 10) earnedBadges.push('level_10'); + if (level >= 25) earnedBadges.push('level_25'); + if (level >= 50) earnedBadges.push('level_50'); + + const streak = profile?.daily_streak || 0; + if (streak >= 7) earnedBadges.push('streak_7'); + if (streak >= 30) earnedBadges.push('streak_30'); + + earnedBadges = [...new Set(earnedBadges)]; + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(`${profile?.username || target.tag}'s Badges`) + .setThumbnail(profile?.avatar_url || target.displayAvatarURL()) + .setTimestamp(); + + if (earnedBadges.length > 0) { + const badgeDisplay = earnedBadges.map(key => { + const info = BADGE_INFO[key]; + if (info) { + return `${info.emoji} **${info.name}**\n${info.description}`; + } + return `šŸ… **${key}**`; + }).join('\n\n'); + + embed.setDescription(badgeDisplay); + embed.addFields({ name: 'Total Badges', value: `${earnedBadges.length}`, inline: true }); + } else { + embed.setDescription('No badges earned yet. Keep engaging to earn badges!'); + } + + const allBadgeKeys = Object.keys(BADGE_INFO); + const lockedBadges = allBadgeKeys.filter(k => !earnedBadges.includes(k)); + + if (lockedBadges.length > 0 && lockedBadges.length <= 6) { + const lockedDisplay = lockedBadges.map(key => { + const info = BADGE_INFO[key]; + return `šŸ”’ ${info.name}`; + }).join(', '); + embed.addFields({ name: 'Locked Badges', value: lockedDisplay }); + } + + await interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Badges error:', error); + await interaction.editReply({ content: 'Failed to fetch badge data.' }); + } + }, +}; diff --git a/aethex-bot/commands/ban.js b/aethex-bot/commands/ban.js new file mode 100644 index 0000000..2bf2734 --- /dev/null +++ b/aethex-bot/commands/ban.js @@ -0,0 +1,93 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('ban') + .setDescription('Ban a user from the server') + .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers) + .addUserOption(option => + option.setName('user') + .setDescription('User to ban') + .setRequired(true) + ) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for ban') + .setRequired(false) + .setMaxLength(500) + ) + .addIntegerOption(option => + option.setName('delete_days') + .setDescription('Days of messages to delete (0-7)') + .setRequired(false) + .setMinValue(0) + .setMaxValue(7) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user'); + const reason = interaction.options.getString('reason') || 'No reason provided'; + const deleteDays = interaction.options.getInteger('delete_days') || 0; + const moderator = interaction.user; + + if (target.id === interaction.user.id) { + return interaction.reply({ content: 'You cannot ban yourself.', ephemeral: true }); + } + + const member = await interaction.guild.members.fetch(target.id).catch(() => null); + + if (member && !member.bannable) { + return interaction.reply({ content: 'I cannot ban this user. They may have higher permissions.', ephemeral: true }); + } + + try { + await target.send({ + embeds: [ + new EmbedBuilder() + .setColor(0xff0000) + .setTitle(`Banned from ${interaction.guild.name}`) + .addFields({ name: 'Reason', value: reason }) + .setTimestamp() + ] + }).catch(() => {}); + + await interaction.guild.members.ban(target, { + reason: `${reason} | Banned by ${moderator.tag}`, + deleteMessageSeconds: deleteDays * 24 * 60 * 60 + }); + + if (supabase) { + try { + await supabase.from('mod_actions').insert({ + guild_id: interaction.guildId, + action: 'ban', + user_id: target.id, + user_tag: target.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: reason, + }); + } catch (e) { + console.warn('Failed to log ban:', e.message); + } + } + + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle('User Banned') + .setThumbnail(target.displayAvatarURL()) + .addFields( + { name: 'User', value: `${target.tag} (${target.id})`, inline: true }, + { name: 'Moderator', value: moderator.tag, inline: true }, + { name: 'Reason', value: reason } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + } catch (error) { + console.error('Ban error:', error); + await interaction.reply({ content: 'Failed to ban user.', ephemeral: true }); + } + }, +}; diff --git a/aethex-bot/commands/config.js b/aethex-bot/commands/config.js new file mode 100644 index 0000000..6c851c3 --- /dev/null +++ b/aethex-bot/commands/config.js @@ -0,0 +1,184 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('config') + .setDescription('Configure server settings') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addSubcommand(sub => + sub.setName('view') + .setDescription('View current server configuration') + ) + .addSubcommand(sub => + sub.setName('welcome') + .setDescription('Set the welcome channel') + .addChannelOption(opt => + opt.setName('channel') + .setDescription('Channel for welcome messages') + .addChannelTypes(ChannelType.GuildText) + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub.setName('goodbye') + .setDescription('Set the goodbye channel') + .addChannelOption(opt => + opt.setName('channel') + .setDescription('Channel for goodbye messages') + .addChannelTypes(ChannelType.GuildText) + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub.setName('modlog') + .setDescription('Set the moderation log channel') + .addChannelOption(opt => + opt.setName('channel') + .setDescription('Channel for mod logs') + .addChannelTypes(ChannelType.GuildText) + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub.setName('levelup') + .setDescription('Set the level-up announcement channel') + .addChannelOption(opt => + opt.setName('channel') + .setDescription('Channel for level-up messages') + .addChannelTypes(ChannelType.GuildText) + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub.setName('autorole') + .setDescription('Set auto-role for new members') + .addRoleOption(opt => + opt.setName('role') + .setDescription('Role to assign on join') + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub.setName('levelrole') + .setDescription('Add a role reward for reaching a level') + .addRoleOption(opt => + opt.setName('role') + .setDescription('Role to give') + .setRequired(true) + ) + .addIntegerOption(opt => + opt.setName('level') + .setDescription('Level required') + .setRequired(true) + .setMinValue(1) + .setMaxValue(100) + ) + ), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ content: 'Database not configured.', ephemeral: true }); + } + + const subcommand = interaction.options.getSubcommand(); + await interaction.deferReply({ ephemeral: true }); + + try { + if (subcommand === 'view') { + const { data: config } = await supabase + .from('server_config') + .select('*') + .eq('guild_id', interaction.guildId) + .single(); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('Server Configuration') + .addFields( + { name: 'Welcome Channel', value: config?.welcome_channel ? `<#${config.welcome_channel}>` : 'Not set', inline: true }, + { name: 'Goodbye Channel', value: config?.goodbye_channel ? `<#${config.goodbye_channel}>` : 'Not set', inline: true }, + { name: 'Mod Log Channel', value: config?.modlog_channel ? `<#${config.modlog_channel}>` : 'Not set', inline: true }, + { name: 'Level-Up Channel', value: config?.level_up_channel ? `<#${config.level_up_channel}>` : 'Not set', inline: true }, + { name: 'Auto Role', value: config?.auto_role ? `<@&${config.auto_role}>` : 'Not set', inline: true } + ) + .setTimestamp(); + + const { data: levelRoles } = await supabase + .from('level_roles') + .select('role_id, level_required') + .eq('guild_id', interaction.guildId) + .order('level_required', { ascending: true }); + + if (levelRoles && levelRoles.length > 0) { + const roleText = levelRoles.map(lr => `Level ${lr.level_required}: <@&${lr.role_id}>`).join('\n'); + embed.addFields({ name: 'Level Roles', value: roleText }); + } + + return interaction.editReply({ embeds: [embed] }); + } + + const updateField = { + welcome: 'welcome_channel', + goodbye: 'goodbye_channel', + modlog: 'modlog_channel', + levelup: 'level_up_channel', + autorole: 'auto_role', + }; + + if (subcommand === 'levelrole') { + const role = interaction.options.getRole('role'); + const level = interaction.options.getInteger('level'); + + await supabase.from('level_roles').upsert({ + guild_id: interaction.guildId, + role_id: role.id, + level_required: level, + }, { onConflict: 'guild_id,role_id' }); + + if (!client.serverConfigs) client.serverConfigs = new Map(); + + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0x00ff00) + .setDescription(`${role} will now be given at level ${level}!`) + ] + }); + } + + const fieldName = updateField[subcommand]; + let value; + + if (subcommand === 'autorole') { + value = interaction.options.getRole('role').id; + } else { + value = interaction.options.getChannel('channel').id; + } + + await supabase.from('server_config').upsert({ + guild_id: interaction.guildId, + [fieldName]: value, + updated_at: new Date().toISOString(), + }, { onConflict: 'guild_id' }); + + if (!client.serverConfigs) client.serverConfigs = new Map(); + const current = client.serverConfigs.get(interaction.guildId) || {}; + current[fieldName] = value; + client.serverConfigs.set(interaction.guildId, current); + + const displayValue = subcommand === 'autorole' ? `<@&${value}>` : `<#${value}>`; + + await interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0x00ff00) + .setDescription(`${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)} set to ${displayValue}!`) + ] + }); + + } catch (error) { + console.error('Config error:', error); + await interaction.editReply({ content: 'Failed to update configuration.' }); + } + }, +}; diff --git a/aethex-bot/commands/daily.js b/aethex-bot/commands/daily.js new file mode 100644 index 0000000..928b2d5 --- /dev/null +++ b/aethex-bot/commands/daily.js @@ -0,0 +1,110 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +const DAILY_XP = 50; +const STREAK_BONUS = 10; +const MAX_STREAK_BONUS = 100; + +module.exports = { + data: new SlashCommandBuilder() + .setName('daily') + .setDescription('Claim your daily XP bonus'), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ content: 'Database not configured.', ephemeral: true }); + } + + await interaction.deferReply(); + + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id') + .eq('discord_id', interaction.user.id) + .single(); + + if (!link) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0xff6b6b) + .setDescription('You need to link your account first! Use `/verify` to get started.') + ] + }); + } + + const { data: profile } = await supabase + .from('user_profiles') + .select('xp, daily_streak, last_daily') + .eq('id', link.user_id) + .single(); + + const now = new Date(); + const lastDaily = profile?.last_daily ? new Date(profile.last_daily) : null; + const currentXp = profile?.xp || 0; + let streak = profile?.daily_streak || 0; + + if (lastDaily) { + const hoursSince = (now - lastDaily) / (1000 * 60 * 60); + + if (hoursSince < 20) { + const nextClaim = new Date(lastDaily.getTime() + 20 * 60 * 60 * 1000); + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0xfbbf24) + .setTitle('Already Claimed!') + .setDescription(`You've already claimed your daily XP.\nNext claim: `) + .addFields({ name: 'Current Streak', value: `šŸ”„ ${streak} days` }) + ] + }); + } + + if (hoursSince > 48) { + streak = 0; + } + } + + streak += 1; + const streakBonus = Math.min(streak * STREAK_BONUS, MAX_STREAK_BONUS); + const totalXp = DAILY_XP + streakBonus; + const newXp = currentXp + totalXp; + + await supabase + .from('user_profiles') + .update({ + xp: newXp, + daily_streak: streak, + last_daily: now.toISOString() + }) + .eq('id', link.user_id); + + const newLevel = Math.floor(Math.sqrt(newXp / 100)); + const oldLevel = Math.floor(Math.sqrt(currentXp / 100)); + + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle('Daily Reward Claimed!') + .setDescription(`You received **+${totalXp} XP**!`) + .addFields( + { name: 'Base XP', value: `+${DAILY_XP}`, inline: true }, + { name: 'Streak Bonus', value: `+${streakBonus}`, inline: true }, + { name: 'Current Streak', value: `šŸ”„ ${streak} days`, inline: true }, + { name: 'Total XP', value: newXp.toLocaleString(), inline: true }, + { name: 'Level', value: `${newLevel}`, inline: true } + ) + .setFooter({ text: 'Come back tomorrow to keep your streak!' }) + .setTimestamp(); + + if (newLevel > oldLevel) { + embed.addFields({ name: 'šŸŽ‰ Level Up!', value: `You reached level ${newLevel}!` }); + } + + await interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Daily error:', error); + await interaction.editReply({ content: 'Failed to claim daily reward.' }); + } + }, +}; diff --git a/aethex-bot/commands/foundation.js b/aethex-bot/commands/foundation.js new file mode 100644 index 0000000..b99851a --- /dev/null +++ b/aethex-bot/commands/foundation.js @@ -0,0 +1,82 @@ +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('foundation') + .setDescription('View AeThex Foundation info and your contribution stats') + .addUserOption(option => + option.setName('user') + .setDescription('User to view (defaults to yourself)') + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user') || interaction.user; + await interaction.deferReply(); + + let contributionData = null; + + if (supabase) { + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id') + .eq('discord_id', target.id) + .single(); + + if (link) { + const { data: contribution } = await supabase + .from('foundation_contributions') + .select('total_donated, volunteer_hours, badges') + .eq('user_id', link.user_id) + .single(); + + contributionData = contribution; + } + } catch (e) { + console.warn('Foundation data fetch failed:', e.message); + } + } + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('AeThex Foundation') + .setDescription('Supporting creators, developers, and the gaming community.') + .setThumbnail(target.displayAvatarURL()) + .addFields( + { name: 'Mission', value: 'Empowering the next generation of creators through education, resources, and community support.' } + ) + .setTimestamp(); + + if (contributionData) { + embed.addFields( + { name: `${target.tag}'s Contributions`, value: '\u200b' }, + { name: 'Total Donated', value: `$${contributionData.total_donated || 0}`, inline: true }, + { name: 'Volunteer Hours', value: `${contributionData.volunteer_hours || 0}h`, inline: true } + ); + + if (contributionData.badges && contributionData.badges.length > 0) { + embed.addFields({ name: 'Badges', value: contributionData.badges.join(', ') }); + } + } else if (target.id === interaction.user.id) { + embed.addFields({ + name: 'Your Contributions', + value: 'Link your account with `/verify` to track your contributions!' + }); + } + + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel('Visit Foundation') + .setStyle(ButtonStyle.Link) + .setURL('https://aethex.foundation'), + new ButtonBuilder() + .setLabel('Donate') + .setStyle(ButtonStyle.Link) + .setURL('https://aethex.foundation/donate') + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); + }, +}; diff --git a/aethex-bot/commands/kick.js b/aethex-bot/commands/kick.js new file mode 100644 index 0000000..2c46be5 --- /dev/null +++ b/aethex-bot/commands/kick.js @@ -0,0 +1,86 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('kick') + .setDescription('Kick a user from the server') + .setDefaultMemberPermissions(PermissionFlagsBits.KickMembers) + .addUserOption(option => + option.setName('user') + .setDescription('User to kick') + .setRequired(true) + ) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for kick') + .setRequired(false) + .setMaxLength(500) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user'); + const reason = interaction.options.getString('reason') || 'No reason provided'; + const moderator = interaction.user; + + const member = await interaction.guild.members.fetch(target.id).catch(() => null); + + if (!member) { + return interaction.reply({ content: 'User not found in this server.', ephemeral: true }); + } + + if (!member.kickable) { + return interaction.reply({ content: 'I cannot kick this user. They may have higher permissions.', ephemeral: true }); + } + + if (target.id === interaction.user.id) { + return interaction.reply({ content: 'You cannot kick yourself.', ephemeral: true }); + } + + try { + await target.send({ + embeds: [ + new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle(`Kicked from ${interaction.guild.name}`) + .addFields({ name: 'Reason', value: reason }) + .setTimestamp() + ] + }).catch(() => {}); + + await member.kick(reason); + + if (supabase) { + try { + await supabase.from('mod_actions').insert({ + guild_id: interaction.guildId, + action: 'kick', + user_id: target.id, + user_tag: target.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: reason, + }); + } catch (e) { + console.warn('Failed to log kick:', e.message); + } + } + + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle('User Kicked') + .setThumbnail(target.displayAvatarURL()) + .addFields( + { name: 'User', value: `${target.tag} (${target.id})`, inline: true }, + { name: 'Moderator', value: moderator.tag, inline: true }, + { name: 'Reason', value: reason } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + } catch (error) { + console.error('Kick error:', error); + await interaction.reply({ content: 'Failed to kick user.', ephemeral: true }); + } + }, +}; diff --git a/aethex-bot/commands/modlog.js b/aethex-bot/commands/modlog.js new file mode 100644 index 0000000..fda333c --- /dev/null +++ b/aethex-bot/commands/modlog.js @@ -0,0 +1,73 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('modlog') + .setDescription('View moderation history for a user') + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + .addUserOption(option => + option.setName('user') + .setDescription('User to check') + .setRequired(true) + ), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ content: 'Database not configured.', ephemeral: true }); + } + + const target = interaction.options.getUser('user'); + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: warnings } = await supabase + .from('warnings') + .select('*') + .eq('guild_id', interaction.guildId) + .eq('user_id', target.id) + .order('created_at', { ascending: false }) + .limit(10); + + const { data: actions } = await supabase + .from('mod_actions') + .select('*') + .eq('guild_id', interaction.guildId) + .eq('user_id', target.id) + .order('created_at', { ascending: false }) + .limit(10); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(`Moderation Log: ${target.tag}`) + .setThumbnail(target.displayAvatarURL()) + .setTimestamp(); + + if ((!warnings || warnings.length === 0) && (!actions || actions.length === 0)) { + embed.setDescription('No moderation history found for this user.'); + } else { + if (warnings && warnings.length > 0) { + const warnText = warnings.map((w, i) => { + const date = new Date(w.created_at).toLocaleDateString(); + return `**${i + 1}.** ${w.reason}\n By: ${w.moderator_tag} | ${date}`; + }).join('\n\n'); + embed.addFields({ name: `Warnings (${warnings.length})`, value: warnText.slice(0, 1024) }); + } + + if (actions && actions.length > 0) { + const actionText = actions.map((a, i) => { + const date = new Date(a.created_at).toLocaleDateString(); + const duration = a.duration_minutes ? ` (${a.duration_minutes}m)` : ''; + return `**${a.action.toUpperCase()}${duration}** - ${a.reason}\n By: ${a.moderator_tag} | ${date}`; + }).join('\n\n'); + embed.addFields({ name: `Actions (${actions.length})`, value: actionText.slice(0, 1024) }); + } + } + + await interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Modlog error:', error); + await interaction.editReply({ content: 'Failed to fetch moderation log.' }); + } + }, +}; diff --git a/aethex-bot/commands/rank.js b/aethex-bot/commands/rank.js new file mode 100644 index 0000000..16f82d4 --- /dev/null +++ b/aethex-bot/commands/rank.js @@ -0,0 +1,86 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('rank') + .setDescription('View your unified level and XP across all platforms') + .addUserOption(option => + option.setName('user') + .setDescription('User to check (defaults to yourself)') + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ content: 'Database not configured.', ephemeral: true }); + } + + const target = interaction.options.getUser('user') || interaction.user; + await interaction.deferReply(); + + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id, primary_arm') + .eq('discord_id', target.id) + .single(); + + if (!link) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0xff6b6b) + .setDescription(`${target.id === interaction.user.id ? 'You are' : `${target.tag} is`} not linked to AeThex. Use \`/verify\` to link your account.`) + ] + }); + } + + const { data: profile } = await supabase + .from('user_profiles') + .select('username, avatar_url, xp, bio') + .eq('id', link.user_id) + .single(); + + const xp = profile?.xp || 0; + const level = Math.floor(Math.sqrt(xp / 100)); + const currentLevelXp = level * level * 100; + const nextLevelXp = (level + 1) * (level + 1) * 100; + const progress = xp - currentLevelXp; + const needed = nextLevelXp - currentLevelXp; + const progressPercent = Math.floor((progress / needed) * 100); + + const progressBar = createProgressBar(progressPercent); + + const { count: rankPosition } = await supabase + .from('user_profiles') + .select('*', { count: 'exact', head: true }) + .gt('xp', xp); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(`${profile?.username || target.tag}'s Rank`) + .setThumbnail(profile?.avatar_url || target.displayAvatarURL()) + .addFields( + { name: 'Level', value: `**${level}**`, inline: true }, + { name: 'Total XP', value: `**${xp.toLocaleString()}**`, inline: true }, + { name: 'Rank', value: `#${(rankPosition || 0) + 1}`, inline: true }, + { name: 'Progress to Next Level', value: `${progressBar}\n${progress.toLocaleString()} / ${needed.toLocaleString()} XP (${progressPercent}%)` }, + { name: 'Primary Realm', value: link.primary_arm || 'None set', inline: true } + ) + .setFooter({ text: 'XP earned across Discord & AeThex platforms' }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Rank error:', error); + await interaction.editReply({ content: 'Failed to fetch rank data.' }); + } + }, +}; + +function createProgressBar(percent) { + const filled = Math.floor(percent / 10); + const empty = 10 - filled; + return 'ā–ˆ'.repeat(filled) + 'ā–‘'.repeat(empty); +} diff --git a/aethex-bot/commands/serverinfo.js b/aethex-bot/commands/serverinfo.js new file mode 100644 index 0000000..2cba9d7 --- /dev/null +++ b/aethex-bot/commands/serverinfo.js @@ -0,0 +1,53 @@ +const { SlashCommandBuilder, EmbedBuilder, ChannelType } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('serverinfo') + .setDescription('View information about this server'), + + async execute(interaction, supabase, client) { + const guild = interaction.guild; + + const textChannels = guild.channels.cache.filter(c => c.type === ChannelType.GuildText).size; + const voiceChannels = guild.channels.cache.filter(c => c.type === ChannelType.GuildVoice).size; + const categories = guild.channels.cache.filter(c => c.type === ChannelType.GuildCategory).size; + + const roles = guild.roles.cache.size - 1; + const emojis = guild.emojis.cache.size; + + const boostLevel = guild.premiumTier; + const boostCount = guild.premiumSubscriptionCount || 0; + + const owner = await guild.fetchOwner().catch(() => null); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(guild.name) + .setThumbnail(guild.iconURL({ size: 256 })) + .addFields( + { name: 'ID', value: guild.id, inline: true }, + { name: 'Owner', value: owner ? owner.user.tag : 'Unknown', inline: true }, + { name: 'Created', value: ``, inline: true }, + { name: 'Members', value: `${guild.memberCount}`, inline: true }, + { name: 'Roles', value: `${roles}`, inline: true }, + { name: 'Emojis', value: `${emojis}`, inline: true }, + { name: 'Text Channels', value: `${textChannels}`, inline: true }, + { name: 'Voice Channels', value: `${voiceChannels}`, inline: true }, + { name: 'Categories', value: `${categories}`, inline: true }, + { name: 'Boost Level', value: `Tier ${boostLevel}`, inline: true }, + { name: 'Boosts', value: `${boostCount}`, inline: true }, + { name: 'Verification', value: guild.verificationLevel.toString(), inline: true } + ) + .setTimestamp(); + + if (guild.description) { + embed.setDescription(guild.description); + } + + if (guild.bannerURL()) { + embed.setImage(guild.bannerURL({ size: 512 })); + } + + await interaction.reply({ embeds: [embed] }); + }, +}; diff --git a/aethex-bot/commands/studio.js b/aethex-bot/commands/studio.js new file mode 100644 index 0000000..ef7ac9f --- /dev/null +++ b/aethex-bot/commands/studio.js @@ -0,0 +1,83 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('studio') + .setDescription('View your AeThex Studio profile and projects') + .addUserOption(option => + option.setName('user') + .setDescription('User to view (defaults to yourself)') + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ content: 'Database not configured.', ephemeral: true }); + } + + const target = interaction.options.getUser('user') || interaction.user; + await interaction.deferReply(); + + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id') + .eq('discord_id', target.id) + .single(); + + if (!link) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0xff6b6b) + .setDescription(`${target.id === interaction.user.id ? 'You are' : `${target.tag} is`} not linked to AeThex.`) + ] + }); + } + + const { data: profile } = await supabase + .from('user_profiles') + .select('username, avatar_url, bio') + .eq('id', link.user_id) + .single(); + + const { data: projects, count: projectCount } = await supabase + .from('studio_projects') + .select('id, title, description, created_at, likes', { count: 'exact' }) + .eq('user_id', link.user_id) + .order('created_at', { ascending: false }) + .limit(5); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(`${profile?.username || target.tag}'s Studio`) + .setThumbnail(profile?.avatar_url || target.displayAvatarURL()) + .setDescription(profile?.bio || 'No bio set') + .addFields( + { name: 'Total Projects', value: `${projectCount || 0}`, inline: true } + ) + .setTimestamp(); + + if (projects && projects.length > 0) { + const projectList = projects.map((p, i) => { + const likes = p.likes || 0; + return `**${i + 1}. ${p.title}**\nā¤ļø ${likes} likes`; + }).join('\n\n'); + embed.addFields({ name: 'Recent Projects', value: projectList }); + } else { + embed.addFields({ name: 'Projects', value: 'No projects yet' }); + } + + embed.addFields({ + name: 'Visit Studio', + value: '[aethex.studio](https://aethex.studio)' + }); + + await interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Studio error:', error); + await interaction.editReply({ content: 'Failed to fetch studio data.' }); + } + }, +}; diff --git a/aethex-bot/commands/timeout.js b/aethex-bot/commands/timeout.js new file mode 100644 index 0000000..2167a49 --- /dev/null +++ b/aethex-bot/commands/timeout.js @@ -0,0 +1,105 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('timeout') + .setDescription('Timeout a user') + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + .addUserOption(option => + option.setName('user') + .setDescription('User to timeout') + .setRequired(true) + ) + .addIntegerOption(option => + option.setName('duration') + .setDescription('Duration in minutes') + .setRequired(true) + .setMinValue(1) + .setMaxValue(40320) + ) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for timeout') + .setRequired(false) + .setMaxLength(500) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user'); + const duration = interaction.options.getInteger('duration'); + const reason = interaction.options.getString('reason') || 'No reason provided'; + const moderator = interaction.user; + + const member = await interaction.guild.members.fetch(target.id).catch(() => null); + + if (!member) { + return interaction.reply({ content: 'User not found in this server.', ephemeral: true }); + } + + if (!member.moderatable) { + return interaction.reply({ content: 'I cannot timeout this user. They may have higher permissions.', ephemeral: true }); + } + + if (target.id === interaction.user.id) { + return interaction.reply({ content: 'You cannot timeout yourself.', ephemeral: true }); + } + + const durationMs = duration * 60 * 1000; + const endsAt = new Date(Date.now() + durationMs); + + try { + await member.timeout(durationMs, `${reason} | By ${moderator.tag}`); + + if (supabase) { + try { + await supabase.from('mod_actions').insert({ + guild_id: interaction.guildId, + action: 'timeout', + user_id: target.id, + user_tag: target.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: reason, + duration_minutes: duration, + }); + } catch (e) { + console.warn('Failed to log timeout:', e.message); + } + } + + const embed = new EmbedBuilder() + .setColor(0xfbbf24) + .setTitle('User Timed Out') + .setThumbnail(target.displayAvatarURL()) + .addFields( + { name: 'User', value: `${target.tag} (${target.id})`, inline: true }, + { name: 'Moderator', value: moderator.tag, inline: true }, + { name: 'Duration', value: `${duration} minutes`, inline: true }, + { name: 'Ends', value: ``, inline: true }, + { name: 'Reason', value: reason } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + try { + await target.send({ + embeds: [ + new EmbedBuilder() + .setColor(0xfbbf24) + .setTitle(`Timed out in ${interaction.guild.name}`) + .addFields( + { name: 'Duration', value: `${duration} minutes` }, + { name: 'Reason', value: reason } + ) + .setTimestamp() + ] + }); + } catch (e) {} + + } catch (error) { + console.error('Timeout error:', error); + await interaction.reply({ content: 'Failed to timeout user.', ephemeral: true }); + } + }, +}; diff --git a/aethex-bot/commands/userinfo.js b/aethex-bot/commands/userinfo.js new file mode 100644 index 0000000..d3234f7 --- /dev/null +++ b/aethex-bot/commands/userinfo.js @@ -0,0 +1,80 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('userinfo') + .setDescription('View information about a user') + .addUserOption(option => + option.setName('user') + .setDescription('User to view (defaults to yourself)') + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user') || interaction.user; + const member = await interaction.guild.members.fetch(target.id).catch(() => null); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(target.tag) + .setThumbnail(target.displayAvatarURL({ size: 256 })) + .addFields( + { name: 'ID', value: target.id, inline: true }, + { name: 'Bot', value: target.bot ? 'Yes' : 'No', inline: true }, + { name: 'Account Created', value: ``, inline: true } + ); + + if (member) { + embed.addFields( + { name: 'Joined Server', value: ``, inline: true }, + { name: 'Nickname', value: member.nickname || 'None', inline: true }, + { name: 'Highest Role', value: member.roles.highest.toString(), inline: true } + ); + + const roles = member.roles.cache + .filter(r => r.id !== interaction.guildId) + .sort((a, b) => b.position - a.position) + .map(r => r.toString()) + .slice(0, 10); + + if (roles.length > 0) { + embed.addFields({ + name: `Roles (${member.roles.cache.size - 1})`, + value: roles.join(', ') + (member.roles.cache.size > 11 ? '...' : '') + }); + } + } + + if (supabase) { + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id, primary_arm, linked_at') + .eq('discord_id', target.id) + .single(); + + if (link) { + const { data: profile } = await supabase + .from('user_profiles') + .select('username, xp') + .eq('id', link.user_id) + .single(); + + embed.addFields( + { name: 'AeThex Linked', value: 'Yes', inline: true }, + { name: 'Platform Username', value: profile?.username || 'Unknown', inline: true }, + { name: 'Realm', value: link.primary_arm || 'None', inline: true } + ); + + if (profile?.xp) { + const level = Math.floor(Math.sqrt(profile.xp / 100)); + embed.addFields({ name: 'Level', value: `${level} (${profile.xp} XP)`, inline: true }); + } + } + } catch (e) {} + } + + embed.setTimestamp(); + await interaction.reply({ embeds: [embed] }); + }, +}; diff --git a/aethex-bot/commands/warn.js b/aethex-bot/commands/warn.js new file mode 100644 index 0000000..75258fb --- /dev/null +++ b/aethex-bot/commands/warn.js @@ -0,0 +1,90 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('warn') + .setDescription('Warn a user') + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + .addUserOption(option => + option.setName('user') + .setDescription('User to warn') + .setRequired(true) + ) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for warning') + .setRequired(true) + .setMaxLength(500) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user'); + const reason = interaction.options.getString('reason'); + const moderator = interaction.user; + + if (target.id === interaction.user.id) { + return interaction.reply({ content: 'You cannot warn yourself.', ephemeral: true }); + } + + if (target.bot) { + return interaction.reply({ content: 'You cannot warn bots.', ephemeral: true }); + } + + let warningCount = 1; + + if (supabase) { + try { + await supabase.from('warnings').insert({ + guild_id: interaction.guildId, + user_id: target.id, + user_tag: target.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: reason, + }); + + const { count } = await supabase + .from('warnings') + .select('*', { count: 'exact', head: true }) + .eq('guild_id', interaction.guildId) + .eq('user_id', target.id); + + warningCount = count || 1; + } catch (e) { + console.warn('Failed to save warning:', e.message); + } + } + + const embed = new EmbedBuilder() + .setColor(0xfbbf24) + .setTitle('User Warned') + .setThumbnail(target.displayAvatarURL()) + .addFields( + { name: 'User', value: `${target.tag} (${target.id})`, inline: true }, + { name: 'Moderator', value: moderator.tag, inline: true }, + { name: 'Warning #', value: `${warningCount}`, inline: true }, + { name: 'Reason', value: reason } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + try { + await target.send({ + embeds: [ + new EmbedBuilder() + .setColor(0xfbbf24) + .setTitle(`Warning in ${interaction.guild.name}`) + .setDescription(`You have been warned by a moderator.`) + .addFields( + { name: 'Reason', value: reason }, + { name: 'Warning #', value: `${warningCount}` } + ) + .setTimestamp() + ] + }); + } catch (e) { + await interaction.followUp({ content: 'Could not DM user about the warning.', ephemeral: true }); + } + }, +}; diff --git a/aethex-bot/listeners/goodbye.js b/aethex-bot/listeners/goodbye.js new file mode 100644 index 0000000..01b40d0 --- /dev/null +++ b/aethex-bot/listeners/goodbye.js @@ -0,0 +1,41 @@ +const { EmbedBuilder } = require('discord.js'); + +module.exports = { + name: 'guildMemberRemove', + + async execute(member, client, supabase) { + if (!supabase) return; + + try { + const { data: config } = await supabase + .from('server_config') + .select('goodbye_channel') + .eq('guild_id', member.guild.id) + .single(); + + if (!config || !config.goodbye_channel) return; + + const channel = await client.channels.fetch(config.goodbye_channel).catch(() => null); + if (!channel) return; + + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle('Goodbye!') + .setDescription(`${member.user.tag} has left the server.`) + .setThumbnail(member.displayAvatarURL({ size: 256 })) + .addFields( + { name: 'Members Now', value: `${member.guild.memberCount}`, inline: true }, + { name: 'Was Member For', value: member.joinedTimestamp + ? `` + : 'Unknown', inline: true } + ) + .setFooter({ text: `ID: ${member.id}` }) + .setTimestamp(); + + await channel.send({ embeds: [embed] }); + + } catch (error) { + console.error('Goodbye error:', error.message); + } + } +}; diff --git a/aethex-bot/listeners/welcome.js b/aethex-bot/listeners/welcome.js new file mode 100644 index 0000000..34a0556 --- /dev/null +++ b/aethex-bot/listeners/welcome.js @@ -0,0 +1,47 @@ +const { EmbedBuilder } = require('discord.js'); + +module.exports = { + name: 'guildMemberAdd', + + async execute(member, client, supabase) { + if (!supabase) return; + + try { + const { data: config } = await supabase + .from('server_config') + .select('welcome_channel, auto_role') + .eq('guild_id', member.guild.id) + .single(); + + if (!config) return; + + if (config.auto_role) { + await member.roles.add(config.auto_role).catch(err => { + console.warn(`Failed to add auto-role: ${err.message}`); + }); + } + + if (config.welcome_channel) { + const channel = await client.channels.fetch(config.welcome_channel).catch(() => null); + if (!channel) return; + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('Welcome!') + .setDescription(`Welcome to **${member.guild.name}**, ${member}!`) + .setThumbnail(member.displayAvatarURL({ size: 256 })) + .addFields( + { name: 'Member #', value: `${member.guild.memberCount}`, inline: true }, + { name: 'Account Created', value: ``, inline: true } + ) + .setFooter({ text: `ID: ${member.id}` }) + .setTimestamp(); + + await channel.send({ embeds: [embed] }); + } + + } catch (error) { + console.error('Welcome error:', error.message); + } + } +}; diff --git a/aethex-bot/listeners/xpTracker.js b/aethex-bot/listeners/xpTracker.js new file mode 100644 index 0000000..42a8475 --- /dev/null +++ b/aethex-bot/listeners/xpTracker.js @@ -0,0 +1,95 @@ +const XP_PER_MESSAGE = 5; +const XP_COOLDOWN_MS = 60000; +const xpCooldowns = new Map(); + +module.exports = { + name: 'messageCreate', + + async execute(message, client, supabase) { + if (!supabase) return; + if (message.author.bot) return; + if (!message.guild) return; + + const discordUserId = message.author.id; + const now = Date.now(); + const lastXp = xpCooldowns.get(discordUserId) || 0; + + if (now - lastXp < XP_COOLDOWN_MS) return; + + try { + const { data: link, error: linkError } = await supabase + .from('discord_links') + .select('user_id') + .eq('discord_id', discordUserId) + .maybeSingle(); + + if (linkError || !link) return; + + const { data: profile, error: profileError } = await supabase + .from('user_profiles') + .select('xp') + .eq('id', link.user_id) + .maybeSingle(); + + if (profileError || !profile) return; + + const currentXp = profile.xp || 0; + const newXp = currentXp + XP_PER_MESSAGE; + const oldLevel = Math.floor(Math.sqrt(currentXp / 100)); + const newLevel = Math.floor(Math.sqrt(newXp / 100)); + + const { error: updateError } = await supabase + .from('user_profiles') + .update({ xp: newXp }) + .eq('id', link.user_id); + + if (updateError) return; + + xpCooldowns.set(discordUserId, now); + + if (newLevel > oldLevel) { + const config = client.serverConfigs?.get(message.guildId); + const levelUpChannelId = config?.level_up_channel; + + const levelUpMessage = `šŸŽ‰ Congratulations ${message.author}! You reached **Level ${newLevel}**!`; + + if (levelUpChannelId) { + const channel = await client.channels.fetch(levelUpChannelId).catch(() => null); + if (channel) { + await channel.send(levelUpMessage); + } + } else { + await message.channel.send(levelUpMessage).catch(() => {}); + } + + await checkLevelRoles(message.member, newLevel, supabase, message.guildId); + } + + } catch (error) { + console.error('XP tracking error:', error.message); + } + } +}; + +async function checkLevelRoles(member, level, supabase, guildId) { + if (!member || !supabase) return; + + try { + const { data: levelRoles, error } = await supabase + .from('level_roles') + .select('role_id, level_required') + .eq('guild_id', guildId) + .lte('level_required', level) + .order('level_required', { ascending: true }); + + if (error || !levelRoles || levelRoles.length === 0) return; + + for (const lr of levelRoles) { + if (!member.roles.cache.has(lr.role_id)) { + await member.roles.add(lr.role_id).catch(() => {}); + } + } + } catch (e) { + // Table may not exist yet - silently ignore + } +} diff --git a/replit.md b/replit.md index 1ebfc7d..2fb536d 100644 --- a/replit.md +++ b/replit.md @@ -1,52 +1,73 @@ # AeThex Unified Bot -A complete Discord bot combining AeThex community features and Sentinel enterprise security in one instance. +A complete Discord bot combining AeThex community features, Sentinel enterprise security, and multi-purpose server management in one instance. ## Overview -AeThex Unified Bot handles both community features AND security: +AeThex Unified Bot handles community features, security, AND general server management: - **Community Features**: User verification, profile linking, realm selection, leaderboards, community posts - **Sentinel Security**: Anti-nuke protection with RAM-based heat tracking - **Federation Sync**: Cross-server role synchronization across 5 realms - **Ticket System**: Support tickets with automatic channel creation -- **Admin Monitoring**: Real-time status, threat monitoring, server overview +- **Moderation**: Full moderation suite (warn, kick, ban, timeout) +- **Leveling System**: Unified XP across Discord and AeThex platform +- **Cross-Platform**: Integration with AeThex.studio and AeThex.foundation ## Tech Stack - **Runtime**: Node.js 20 - **Framework**: discord.js v14 -- **Database**: Supabase (optional - for user verification and community features) +- **Database**: Supabase (for verification, XP, moderation logs) - **Health Endpoint**: HTTP server on port 8080 ## Project Structure ``` aethex-bot/ -ā”œā”€ā”€ bot.js # Main entry point (merged: original + Sentinel) +ā”œā”€ā”€ bot.js # Main entry point ā”œā”€ā”€ package.json -ā”œā”€ā”€ Dockerfile # Docker deployment config -ā”œā”€ā”€ discloud.config # DisCloud hosting config -ā”œā”€ā”€ DEPLOYMENT_GUIDE.md # Deployment documentation +ā”œā”€ā”€ public/ +│ └── dashboard.html # Web dashboard ā”œā”€ā”€ commands/ │ ā”œā”€ā”€ admin.js # /admin status|heat|servers|threats|federation +│ ā”œā”€ā”€ announce.js # /announce - cross-server announcements +│ ā”œā”€ā”€ auditlog.js # /auditlog - admin action history +│ ā”œā”€ā”€ avatar.js # /avatar - get user avatar +│ ā”œā”€ā”€ badges.js # /badges - view earned badges +│ ā”œā”€ā”€ ban.js # /ban - ban users +│ ā”œā”€ā”€ config.js # /config - server settings +│ ā”œā”€ā”€ daily.js # /daily - claim daily XP │ ā”œā”€ā”€ federation.js # /federation link|unlink|list +│ ā”œā”€ā”€ foundation.js # /foundation - foundation stats │ ā”œā”€ā”€ help.js # /help - command list +│ ā”œā”€ā”€ kick.js # /kick - kick users │ ā”œā”€ā”€ leaderboard.js # /leaderboard - top contributors +│ ā”œā”€ā”€ modlog.js # /modlog - user mod history +│ ā”œā”€ā”€ poll.js # /poll - community polls │ ā”œā”€ā”€ post.js # /post - community feed posts │ ā”œā”€ā”€ profile.js # /profile - view linked profile +│ ā”œā”€ā”€ rank.js # /rank - view level and XP │ ā”œā”€ā”€ refresh-roles.js # /refresh-roles - sync roles +│ ā”œā”€ā”€ serverinfo.js # /serverinfo - server stats │ ā”œā”€ā”€ set-realm.js # /set-realm - choose primary realm │ ā”œā”€ā”€ stats.js # /stats - user statistics │ ā”œā”€ā”€ status.js # /status - network overview +│ ā”œā”€ā”€ studio.js # /studio - studio profile │ ā”œā”€ā”€ ticket.js # /ticket create|close +│ ā”œā”€ā”€ timeout.js # /timeout - timeout users │ ā”œā”€ā”€ unlink.js # /unlink - disconnect account +│ ā”œā”€ā”€ userinfo.js # /userinfo - user details │ ā”œā”€ā”€ verify-role.js # /verify-role - check roles -│ └── verify.js # /verify - link account +│ ā”œā”€ā”€ verify.js # /verify - link account +│ └── warn.js # /warn - warn users ā”œā”€ā”€ events/ │ └── messageCreate.js # Message event handler ā”œā”€ā”€ listeners/ │ ā”œā”€ā”€ feedSync.js # Community feed sync +│ ā”œā”€ā”€ welcome.js # Welcome messages + auto-role +│ ā”œā”€ā”€ goodbye.js # Goodbye messages +│ ā”œā”€ā”€ xpTracker.js # XP tracking on messages │ └── sentinel/ │ ā”œā”€ā”€ antiNuke.js # Channel delete monitor │ ā”œā”€ā”€ roleDelete.js # Role delete monitor @@ -56,9 +77,9 @@ aethex-bot/ └── register-commands.js # Slash command registration ``` -## Commands (14 Total) +## Commands (29 Total) -### Community Commands (10) +### Community Commands | Command | Description | |---------|-------------| | `/verify` | Link your Discord account to AeThex | @@ -72,14 +93,56 @@ aethex-bot/ | `/post` | Create a community feed post | | `/help` | View all bot commands | -### Sentinel Commands (4) +### Leveling & Engagement +| Command | Description | +|---------|-------------| +| `/rank` | View your level and unified XP | +| `/daily` | Claim daily XP bonus | +| `/badges` | View earned badges across platforms | + +### Moderation +| Command | Description | +|---------|-------------| +| `/warn @user [reason]` | Warn a user | +| `/kick @user [reason]` | Kick a user | +| `/ban @user [reason]` | Ban a user | +| `/timeout @user [minutes] [reason]` | Timeout a user | +| `/modlog @user` | View moderation history | + +### Utility +| Command | Description | +|---------|-------------| +| `/userinfo [@user]` | View user information | +| `/serverinfo` | View server statistics | +| `/avatar [@user]` | Get user's avatar | + +### Admin & Config +| Command | Description | +|---------|-------------| +| `/config view` | View server configuration | +| `/config welcome #channel` | Set welcome channel | +| `/config goodbye #channel` | Set goodbye channel | +| `/config modlog #channel` | Set mod log channel | +| `/config levelup #channel` | Set level-up announcement channel | +| `/config autorole @role` | Set auto-role for new members | +| `/config levelrole @role [level]` | Add level-based role reward | +| `/announce [title] [message]` | Send cross-server announcement | +| `/poll [question] [options]` | Create community poll | +| `/auditlog` | View admin action history | + +### Cross-Platform +| Command | Description | +|---------|-------------| +| `/studio [@user]` | View AeThex Studio profile | +| `/foundation [@user]` | View Foundation contributions | + +### Sentinel Security | Command | Description | |---------|-------------| | `/admin status` | View bot status and statistics | | `/admin heat @user` | Check heat level of a user | | `/admin servers` | View all connected servers | | `/admin threats` | View active threat monitor | -| `/admin federation` | View federation role mappings | | `/federation link @role` | Link a role for cross-server sync | | `/federation unlink @role` | Remove a role from sync | | `/federation list` | List all linked roles | @@ -87,63 +150,94 @@ aethex-bot/ | `/ticket close` | Close the current ticket | | `/status` | View network status | -## Sentinel Security System +## Unified XP System -Anti-nuke system using RAM-based heat tracking for instant response: +XP is earned across all platforms and stored in a single profile: -- **Heat Threshold**: 3 dangerous actions in 10 seconds triggers auto-ban -- **Monitored Actions**: Channel delete, role delete, member ban, member kick -- **Alerts**: Sends to configured alert channel and DMs server owner -- **Whitelist**: Set `WHITELISTED_USERS` env var for trusted users +- **Discord Messages**: +5 XP per message (60s cooldown) +- **Daily Claims**: +50 XP base + streak bonus (up to +100) +- **Platform Activity**: Posts, likes, comments on AeThex sites + +Level formula: `level = floor(sqrt(xp / 100))` + +## Supabase Tables Required + +```sql +-- Server configuration +CREATE TABLE server_config ( + guild_id TEXT PRIMARY KEY, + welcome_channel TEXT, + goodbye_channel TEXT, + modlog_channel TEXT, + level_up_channel TEXT, + auto_role TEXT, + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Warnings +CREATE TABLE warnings ( + id SERIAL PRIMARY KEY, + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + user_tag TEXT, + moderator_id TEXT NOT NULL, + moderator_tag TEXT, + reason TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Moderation actions +CREATE TABLE mod_actions ( + id SERIAL PRIMARY KEY, + guild_id TEXT NOT NULL, + action TEXT NOT NULL, + user_id TEXT NOT NULL, + user_tag TEXT, + moderator_id TEXT NOT NULL, + moderator_tag TEXT, + reason TEXT, + duration_minutes INTEGER, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Level roles +CREATE TABLE level_roles ( + guild_id TEXT NOT NULL, + role_id TEXT NOT NULL, + level_required INTEGER NOT NULL, + PRIMARY KEY (guild_id, role_id) +); + +-- Add to user_profiles (if not exists) +ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS xp INTEGER DEFAULT 0; +ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS daily_streak INTEGER DEFAULT 0; +ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS last_daily TIMESTAMPTZ; +ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS badges JSONB DEFAULT '[]'; +``` ## Environment Variables ### Required - `DISCORD_BOT_TOKEN` - Bot token from Discord Developer Portal -- `DISCORD_CLIENT_ID` - Application ID (e.g., 578971245454950421) +- `DISCORD_CLIENT_ID` - Application ID -### Optional - Supabase (for community features) +### Optional - Supabase - `SUPABASE_URL` - Supabase project URL - `SUPABASE_SERVICE_ROLE` - Supabase service role key ### Optional - Federation -- `HUB_GUILD_ID` - Main hub server -- `LABS_GUILD_ID`, `GAMEFORGE_GUILD_ID`, `CORP_GUILD_ID`, `FOUNDATION_GUILD_ID` +- `HUB_GUILD_ID`, `LABS_GUILD_ID`, `GAMEFORGE_GUILD_ID`, `CORP_GUILD_ID`, `FOUNDATION_GUILD_ID` ### Optional - Security - `WHITELISTED_USERS` - Comma-separated user IDs to skip heat tracking - `ALERT_CHANNEL_ID` - Channel for security alerts - -### Optional - Feed Sync -- `DISCORD_FEED_CHANNEL_ID` - Channel for community feed -- `DISCORD_FEED_GUILD_ID` - Guild for community feed -- `DISCORD_MAIN_CHAT_CHANNELS` - Comma-separated channel IDs +- `EXTRA_WHITELISTED_GUILDS` - Additional whitelisted server IDs ## Health Endpoints -**GET /health** (port 8080) -```json -{ - "status": "online", - "guilds": 8, - "commands": 14, - "uptime": 3600, - "heatMapSize": 0, - "supabaseConnected": false, - "timestamp": "2025-12-07T23:00:00.000Z" -} -``` - -**GET /stats** (port 8080) -```json -{ - "guilds": [...], - "totalMembers": 500, - "uptime": 3600, - "activeTickets": 0, - "heatEvents": 0 -} -``` +**GET /health** - Bot health status +**GET /stats** - Server statistics +**GET /dashboard** - Web dashboard ## Running the Bot @@ -153,21 +247,18 @@ npm install npm start ``` -Commands are registered automatically on startup or via POST to `/register-commands`. - ## Current Status - Bot running as AeThex#9389 -- Connected to 8 servers -- 14 commands loaded -- 4 Sentinel listeners active -- Health endpoint on port 8080 -- Supabase optional (community features limited when not configured) +- 29 commands loaded +- Unified XP system active +- Welcome/goodbye system active +- Moderation suite active +- Cross-platform integration ready ## Workflow - **Name**: AeThex Unified Bot - **Command**: `cd aethex-bot && npm start` -- **Runtime**: Node.js 20 (no Python required) -- **Deployment**: VM target with `sh -c "cd aethex-bot && npm start"` +- **Runtime**: Node.js 20 - **Status**: Running