const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('xp-settings') .setDescription('Configure XP settings for your server') .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .addSubcommand(sub => sub.setName('view') .setDescription('View current XP settings')) .addSubcommand(sub => sub.setName('message-xp') .setDescription('Set XP earned per message') .addIntegerOption(opt => opt.setName('amount') .setDescription('XP per message (1-50)') .setRequired(true) .setMinValue(1) .setMaxValue(50))) .addSubcommand(sub => sub.setName('cooldown') .setDescription('Set cooldown between XP gains (seconds)') .addIntegerOption(opt => opt.setName('seconds') .setDescription('Cooldown in seconds (10-300)') .setRequired(true) .setMinValue(10) .setMaxValue(300))) .addSubcommand(sub => sub.setName('multiplier-role') .setDescription('Add/remove a role with XP multiplier') .addRoleOption(opt => opt.setName('role') .setDescription('The role to add/remove multiplier') .setRequired(true)) .addNumberOption(opt => opt.setName('multiplier') .setDescription('XP multiplier (1.1-5.0, or 0 to remove)') .setRequired(true) .setMinValue(0) .setMaxValue(5))) .addSubcommand(sub => sub.setName('bonus-channel') .setDescription('Add/remove a channel with bonus XP') .addChannelOption(opt => opt.setName('channel') .setDescription('The channel to add/remove bonus') .setRequired(true)) .addNumberOption(opt => opt.setName('multiplier') .setDescription('XP multiplier (1.1-5.0, or 0 to remove)') .setRequired(true) .setMinValue(0) .setMaxValue(5))) .addSubcommand(sub => sub.setName('toggle') .setDescription('Enable or disable XP system') .addBooleanOption(opt => opt.setName('enabled') .setDescription('Enable or disable XP') .setRequired(true))) .addSubcommand(sub => sub.setName('level-curve') .setDescription('Set leveling difficulty curve') .addStringOption(opt => opt.setName('curve') .setDescription('Leveling curve type') .setRequired(true) .addChoices( { name: 'Easy - Faster leveling', value: 'easy' }, { name: 'Normal - Standard leveling', value: 'normal' }, { name: 'Hard - Slower leveling', value: 'hard' } ))) .addSubcommand(sub => sub.setName('reaction-xp') .setDescription('Configure XP for reactions') .addIntegerOption(opt => opt.setName('received') .setDescription('XP for receiving a reaction (0-20)') .setRequired(true) .setMinValue(0) .setMaxValue(20)) .addIntegerOption(opt => opt.setName('given') .setDescription('XP for giving a reaction (0-10)') .setRequired(true) .setMinValue(0) .setMaxValue(10))) .addSubcommand(sub => sub.setName('reaction-cooldown') .setDescription('Set cooldown between reaction XP gains') .addIntegerOption(opt => opt.setName('seconds') .setDescription('Cooldown in seconds (5-120)') .setRequired(true) .setMinValue(5) .setMaxValue(120))) .addSubcommand(sub => sub.setName('reaction-toggle') .setDescription('Enable or disable reaction XP') .addBooleanOption(opt => opt.setName('enabled') .setDescription('Enable or disable reaction XP') .setRequired(true))) .addSubcommand(sub => sub.setName('voice-xp') .setDescription('Set XP earned per minute in voice channels') .addIntegerOption(opt => opt.setName('amount') .setDescription('XP per minute (1-20)') .setRequired(true) .setMinValue(1) .setMaxValue(20))) .addSubcommand(sub => sub.setName('voice-cooldown') .setDescription('Set cooldown between voice XP grants') .addIntegerOption(opt => opt.setName('seconds') .setDescription('Cooldown in seconds (30-300)') .setRequired(true) .setMinValue(30) .setMaxValue(300))) .addSubcommand(sub => sub.setName('voice-toggle') .setDescription('Enable or disable voice XP') .addBooleanOption(opt => opt.setName('enabled') .setDescription('Enable or disable voice XP') .setRequired(true))) .addSubcommand(sub => sub.setName('levelup-channel') .setDescription('Set a dedicated channel for level-up announcements') .addChannelOption(opt => opt.setName('channel') .setDescription('Channel for level-ups (leave empty to use current channel)') .setRequired(false))) .addSubcommand(sub => sub.setName('levelup-message') .setDescription('Set custom level-up message (use {user}, {level}, {xp}, {server})') .addStringOption(opt => opt.setName('message') .setDescription('Custom message with placeholders') .setRequired(true) .setMaxLength(500))) .addSubcommand(sub => sub.setName('levelup-dm') .setDescription('Toggle DM notifications for level-ups instead of channel') .addBooleanOption(opt => opt.setName('enabled') .setDescription('Send level-up via DM') .setRequired(true))) .addSubcommand(sub => sub.setName('levelup-embed') .setDescription('Toggle using embeds for level-up announcements') .addBooleanOption(opt => opt.setName('enabled') .setDescription('Use embed for level-ups') .setRequired(true)) .addStringOption(opt => opt.setName('color') .setDescription('Embed color (hex, e.g., #5865F2)') .setRequired(false))) .addSubcommand(sub => sub.setName('levelup-reset') .setDescription('Reset level-up settings to defaults')), async execute(interaction, client, supabase) { if (!supabase) { return interaction.reply({ content: '❌ Database not configured. XP settings require Supabase.', ephemeral: true }); } const guildId = interaction.guildId; const subcommand = interaction.options.getSubcommand(); // Get current config or create default let config = await getXpConfig(supabase, guildId); switch (subcommand) { case 'view': return handleView(interaction, config); case 'message-xp': return handleMessageXp(interaction, supabase, guildId, config); case 'cooldown': return handleCooldown(interaction, supabase, guildId, config); case 'multiplier-role': return handleMultiplierRole(interaction, supabase, guildId, config); case 'bonus-channel': return handleBonusChannel(interaction, supabase, guildId, config); case 'toggle': return handleToggle(interaction, supabase, guildId, config); case 'level-curve': return handleLevelCurve(interaction, supabase, guildId, config); case 'reaction-xp': return handleReactionXp(interaction, supabase, guildId, config); case 'reaction-cooldown': return handleReactionCooldown(interaction, supabase, guildId, config); case 'reaction-toggle': return handleReactionToggle(interaction, supabase, guildId, config); case 'voice-xp': return handleVoiceXp(interaction, supabase, guildId, config); case 'voice-cooldown': return handleVoiceCooldown(interaction, supabase, guildId, config); case 'voice-toggle': return handleVoiceToggle(interaction, supabase, guildId, config); case 'levelup-channel': return handleLevelupChannel(interaction, supabase, guildId, config); case 'levelup-message': return handleLevelupMessage(interaction, supabase, guildId, config); case 'levelup-dm': return handleLevelupDm(interaction, supabase, guildId, config); case 'levelup-embed': return handleLevelupEmbed(interaction, supabase, guildId, config); case 'levelup-reset': return handleLevelupReset(interaction, supabase, guildId, config); } } }; async function getXpConfig(supabase, guildId) { try { const { data, error } = await supabase .from('xp_config') .select('*') .eq('guild_id', guildId) .maybeSingle(); if (error) throw error; if (!data) { // Return default config return { guild_id: guildId, message_xp: 5, message_cooldown: 60, multiplier_roles: [], bonus_channels: [], level_curve: 'normal', xp_enabled: true, reaction_xp_enabled: true, reaction_xp_received: 3, reaction_xp_given: 1, reaction_cooldown: 30, voice_xp_enabled: true, voice_xp: 2, voice_cooldown: 60, levelup_channel_id: null, levelup_message: 'πŸŽ‰ Congratulations {user}! You reached **Level {level}**!', levelup_dm: false, levelup_embed: false, levelup_embed_color: '#5865F2' }; } return data; } catch (e) { console.error('Failed to get XP config:', e.message); return { guild_id: guildId, message_xp: 5, message_cooldown: 60, multiplier_roles: [], bonus_channels: [], level_curve: 'normal', xp_enabled: true, reaction_xp_enabled: true, reaction_xp_received: 3, reaction_xp_given: 1, reaction_cooldown: 30, voice_xp_enabled: true, voice_xp: 2, voice_cooldown: 60, levelup_channel_id: null, levelup_message: 'πŸŽ‰ Congratulations {user}! You reached **Level {level}**!', levelup_dm: false, levelup_embed: false, levelup_embed_color: '#5865F2' }; } } async function saveXpConfig(supabase, guildId, config) { try { const { error } = await supabase .from('xp_config') .upsert({ guild_id: guildId, message_xp: config.message_xp, message_cooldown: config.message_cooldown, multiplier_roles: config.multiplier_roles, bonus_channels: config.bonus_channels, level_curve: config.level_curve, xp_enabled: config.xp_enabled, reaction_xp_enabled: config.reaction_xp_enabled, reaction_xp_received: config.reaction_xp_received, reaction_xp_given: config.reaction_xp_given, reaction_cooldown: config.reaction_cooldown, voice_xp_enabled: config.voice_xp_enabled, voice_xp: config.voice_xp, voice_cooldown: config.voice_cooldown, levelup_channel_id: config.levelup_channel_id, levelup_message: config.levelup_message, levelup_dm: config.levelup_dm, levelup_embed: config.levelup_embed, levelup_embed_color: config.levelup_embed_color, updated_at: new Date().toISOString() }); if (error) throw error; return true; } catch (e) { console.error('Failed to save XP config:', e.message); return false; } } async function handleView(interaction, config) { const multiplierRoles = config.multiplier_roles || []; const bonusChannels = config.bonus_channels || []; const rolesText = multiplierRoles.length > 0 ? multiplierRoles.map(r => `<@&${r.role_id}> β†’ ${r.multiplier}x`).join('\n') : 'None configured'; const channelsText = bonusChannels.length > 0 ? bonusChannels.map(c => `<#${c.channel_id}> β†’ ${c.multiplier}x`).join('\n') : 'None configured'; const curveInfo = { easy: 'Easy (50 XP per levelΒ²)', normal: 'Normal (100 XP per levelΒ²)', hard: 'Hard (200 XP per levelΒ²)' }; const reactionEnabled = config.reaction_xp_enabled !== false; const reactionReceived = config.reaction_xp_received ?? 3; const reactionGiven = config.reaction_xp_given ?? 1; const reactionCooldown = config.reaction_cooldown ?? 30; const voiceEnabled = config.voice_xp_enabled !== false; const voiceXp = config.voice_xp ?? 2; const voiceCooldown = config.voice_cooldown ?? 60; const levelupChannel = config.levelup_channel_id ? `<#${config.levelup_channel_id}>` : 'Current channel'; const levelupMessage = config.levelup_message || 'πŸŽ‰ Congratulations {user}! You reached **Level {level}**!'; const levelupDm = config.levelup_dm === true; const levelupEmbed = config.levelup_embed === true; const levelupColor = config.levelup_embed_color || '#5865F2'; const embed = new EmbedBuilder() .setTitle('βš™οΈ XP Settings') .setColor(config.xp_enabled ? 0x00ff88 : 0xff4444) .addFields( { name: 'πŸ“Š Status', value: config.xp_enabled ? 'βœ… Enabled' : '❌ Disabled', inline: true }, { name: 'πŸ’¬ Message XP', value: `${config.message_xp} XP`, inline: true }, { name: '⏱️ Cooldown', value: `${config.message_cooldown}s`, inline: true }, { name: 'πŸ“ˆ Level Curve', value: curveInfo[config.level_curve] || 'Normal', inline: false }, { name: '🎭 Multiplier Roles', value: rolesText, inline: false }, { name: 'πŸ“’ Bonus Channels', value: channelsText, inline: false }, { name: 'πŸ˜€ Reaction XP', value: reactionEnabled ? 'βœ… Enabled' : '❌ Disabled', inline: true }, { name: 'πŸ“₯ Receive Reaction', value: `${reactionReceived} XP`, inline: true }, { name: 'πŸ“€ Give Reaction', value: `${reactionGiven} XP`, inline: true }, { name: '⏳ Reaction Cooldown', value: `${reactionCooldown}s`, inline: true }, { name: '🎀 Voice XP', value: voiceEnabled ? 'βœ… Enabled' : '❌ Disabled', inline: true }, { name: 'πŸ”Š XP per Minute', value: `${voiceXp} XP`, inline: true }, { name: '⏰ Voice Cooldown', value: `${voiceCooldown}s`, inline: true }, { name: '\u200B', value: '**Level-Up Announcements**', inline: false }, { name: 'πŸ“£ Channel', value: levelupChannel, inline: true }, { name: 'πŸ’Œ DM Mode', value: levelupDm ? 'βœ… Enabled' : '❌ Disabled', inline: true }, { name: 'πŸ–ΌοΈ Use Embed', value: levelupEmbed ? `βœ… (${levelupColor})` : '❌ Disabled', inline: true }, { name: 'πŸ’¬ Message', value: `\`${levelupMessage.slice(0, 100)}${levelupMessage.length > 100 ? '...' : ''}\``, inline: false } ) .setFooter({ text: 'Use /xp-settings subcommands to modify | Placeholders: {user}, {level}, {xp}, {server}' }) .setTimestamp(); return interaction.reply({ embeds: [embed] }); } async function handleMessageXp(interaction, supabase, guildId, config) { const amount = interaction.options.getInteger('amount'); config.message_xp = amount; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `βœ… Message XP set to **${amount} XP** per message.`, ephemeral: true }); } async function handleCooldown(interaction, supabase, guildId, config) { const seconds = interaction.options.getInteger('seconds'); config.message_cooldown = seconds; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `βœ… XP cooldown set to **${seconds} seconds**.`, ephemeral: true }); } async function handleMultiplierRole(interaction, supabase, guildId, config) { const role = interaction.options.getRole('role'); const multiplier = interaction.options.getNumber('multiplier'); let roles = config.multiplier_roles || []; if (multiplier === 0) { // Remove role roles = roles.filter(r => r.role_id !== role.id); config.multiplier_roles = roles; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `βœ… Removed XP multiplier from ${role}.`, ephemeral: true }); } // Add or update role const existing = roles.findIndex(r => r.role_id === role.id); if (existing >= 0) { roles[existing].multiplier = multiplier; } else { roles.push({ role_id: role.id, multiplier }); } config.multiplier_roles = roles; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `βœ… ${role} now has a **${multiplier}x** XP multiplier.`, ephemeral: true }); } async function handleBonusChannel(interaction, supabase, guildId, config) { const channel = interaction.options.getChannel('channel'); const multiplier = interaction.options.getNumber('multiplier'); let channels = config.bonus_channels || []; if (multiplier === 0) { // Remove channel channels = channels.filter(c => c.channel_id !== channel.id); config.bonus_channels = channels; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `βœ… Removed bonus XP from ${channel}.`, ephemeral: true }); } // Add or update channel const existing = channels.findIndex(c => c.channel_id === channel.id); if (existing >= 0) { channels[existing].multiplier = multiplier; } else { channels.push({ channel_id: channel.id, multiplier }); } config.bonus_channels = channels; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `βœ… ${channel} now has a **${multiplier}x** XP bonus.`, ephemeral: true }); } async function handleToggle(interaction, supabase, guildId, config) { const enabled = interaction.options.getBoolean('enabled'); config.xp_enabled = enabled; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: enabled ? 'βœ… XP system is now **enabled**.' : '❌ XP system is now **disabled**.', ephemeral: true }); } async function handleLevelCurve(interaction, supabase, guildId, config) { const curve = interaction.options.getString('curve'); config.level_curve = curve; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } const curveNames = { easy: 'Easy (faster leveling)', normal: 'Normal (standard leveling)', hard: 'Hard (slower leveling)' }; return interaction.reply({ content: `βœ… Level curve set to **${curveNames[curve]}**.`, ephemeral: true }); } async function handleReactionXp(interaction, supabase, guildId, config) { const received = interaction.options.getInteger('received'); const given = interaction.options.getInteger('given'); config.reaction_xp_received = received; config.reaction_xp_given = given; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `βœ… Reaction XP set: **${received} XP** for receiving, **${given} XP** for giving.`, ephemeral: true }); } async function handleReactionCooldown(interaction, supabase, guildId, config) { const seconds = interaction.options.getInteger('seconds'); config.reaction_cooldown = seconds; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `βœ… Reaction XP cooldown set to **${seconds} seconds**.`, ephemeral: true }); } async function handleReactionToggle(interaction, supabase, guildId, config) { const enabled = interaction.options.getBoolean('enabled'); config.reaction_xp_enabled = enabled; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: enabled ? 'βœ… Reaction XP is now **enabled**.' : '❌ Reaction XP is now **disabled**.', ephemeral: true }); } async function handleVoiceXp(interaction, supabase, guildId, config) { const amount = interaction.options.getInteger('amount'); config.voice_xp = amount; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `βœ… Voice XP set to **${amount} XP** per minute.`, ephemeral: true }); } async function handleVoiceCooldown(interaction, supabase, guildId, config) { const seconds = interaction.options.getInteger('seconds'); config.voice_cooldown = seconds; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `βœ… Voice XP cooldown set to **${seconds} seconds**.`, ephemeral: true }); } async function handleVoiceToggle(interaction, supabase, guildId, config) { const enabled = interaction.options.getBoolean('enabled'); config.voice_xp_enabled = enabled; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: enabled ? 'βœ… Voice XP is now **enabled**.' : '❌ Voice XP is now **disabled**.', ephemeral: true }); } async function handleLevelupChannel(interaction, supabase, guildId, config) { const channel = interaction.options.getChannel('channel'); if (!channel) { config.levelup_channel_id = null; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: 'βœ… Level-up announcements will now appear in the channel where the user leveled up.', ephemeral: true }); } config.levelup_channel_id = channel.id; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `βœ… Level-up announcements will now be sent to ${channel}.`, ephemeral: true }); } async function handleLevelupMessage(interaction, supabase, guildId, config) { const message = interaction.options.getString('message'); config.levelup_message = message; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } const preview = message .replace(/{user}/g, interaction.user.toString()) .replace(/{username}/g, interaction.user.username) .replace(/{level}/g, '10') .replace(/{xp}/g, '10,000') .replace(/{server}/g, interaction.guild.name); return interaction.reply({ content: `βœ… Level-up message updated!\n\n**Preview:**\n${preview}`, ephemeral: true }); } async function handleLevelupDm(interaction, supabase, guildId, config) { const enabled = interaction.options.getBoolean('enabled'); config.levelup_dm = enabled; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: enabled ? 'βœ… Level-up notifications will now be sent via **DM** to users.' : 'βœ… Level-up notifications will now be sent in the **channel**.', ephemeral: true }); } async function handleLevelupEmbed(interaction, supabase, guildId, config) { const enabled = interaction.options.getBoolean('enabled'); const color = interaction.options.getString('color'); config.levelup_embed = enabled; if (color) { if (!/^#[0-9A-Fa-f]{6}$/.test(color)) { return interaction.reply({ content: '❌ Invalid color format. Please use hex format like `#5865F2`.', ephemeral: true }); } config.levelup_embed_color = color; } const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } if (enabled) { return interaction.reply({ content: `βœ… Level-up announcements will now use **embeds** with color \`${config.levelup_embed_color}\`.`, ephemeral: true }); } else { return interaction.reply({ content: 'βœ… Level-up announcements will now use **plain text** messages.', ephemeral: true }); } } async function handleLevelupReset(interaction, supabase, guildId, config) { config.levelup_channel_id = null; config.levelup_message = 'πŸŽ‰ Congratulations {user}! You reached **Level {level}**!'; config.levelup_dm = false; config.levelup_embed = false; config.levelup_embed_color = '#5865F2'; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: 'βœ… Level-up announcement settings have been reset to defaults.', ephemeral: true }); }