diff --git a/aethex-bot/commands/activity-roles.js b/aethex-bot/commands/activity-roles.js new file mode 100644 index 0000000..fc914dd --- /dev/null +++ b/aethex-bot/commands/activity-roles.js @@ -0,0 +1,282 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('activity-roles') + .setDescription('Manage automatic role rewards for activity milestones') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addSubcommand(sub => + sub.setName('add') + .setDescription('Add a role reward for an activity milestone') + .addRoleOption(opt => + opt.setName('role') + .setDescription('The role to award') + .setRequired(true)) + .addStringOption(opt => + opt.setName('type') + .setDescription('Type of activity milestone') + .setRequired(true) + .addChoices( + { name: 'Messages - Total messages sent', value: 'messages' }, + { name: 'Voice Hours - Time in voice channels', value: 'voice_hours' }, + { name: 'Daily Streak - Consecutive daily claims', value: 'daily_streak' }, + { name: 'Reactions Given - Reactions added to messages', value: 'reactions_given' }, + { name: 'Reactions Received - Reactions on your messages', value: 'reactions_received' }, + { name: 'Commands Used - Bot commands used', value: 'commands_used' } + )) + .addIntegerOption(opt => + opt.setName('value') + .setDescription('Milestone value (message count, hours, streak days, etc.)') + .setRequired(true) + .setMinValue(1)) + .addBooleanOption(opt => + opt.setName('stack') + .setDescription('Keep previous milestone roles (true) or replace them (false)') + .setRequired(false))) + .addSubcommand(sub => + sub.setName('remove') + .setDescription('Remove an activity role reward') + .addRoleOption(opt => + opt.setName('role') + .setDescription('The role to remove from rewards') + .setRequired(true))) + .addSubcommand(sub => + sub.setName('list') + .setDescription('View all configured activity role rewards')) + .addSubcommand(sub => + sub.setName('clear') + .setDescription('Clear all role rewards for an activity type') + .addStringOption(opt => + opt.setName('type') + .setDescription('Type of activity to clear') + .setRequired(true) + .addChoices( + { name: 'Message roles', value: 'messages' }, + { name: 'Voice hour roles', value: 'voice_hours' }, + { name: 'Daily streak roles', value: 'daily_streak' }, + { name: 'Reactions given roles', value: 'reactions_given' }, + { name: 'Reactions received roles', value: 'reactions_received' }, + { name: 'Commands used roles', value: 'commands_used' }, + { name: 'All activity roles', value: 'all' } + ))), + + async execute(interaction, client, supabase) { + if (!supabase) { + return interaction.reply({ + content: '❌ Database not configured. Activity roles require Supabase.', + ephemeral: true + }); + } + + const guildId = interaction.guildId; + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand) { + case 'add': + return handleAdd(interaction, supabase, guildId); + case 'remove': + return handleRemove(interaction, supabase, guildId); + case 'list': + return handleList(interaction, supabase, guildId); + case 'clear': + return handleClear(interaction, supabase, guildId); + } + } +}; + +const ACTIVITY_TYPES = ['messages', 'voice_hours', 'daily_streak', 'reactions_given', 'reactions_received', 'commands_used']; + +async function handleAdd(interaction, supabase, guildId) { + const role = interaction.options.getRole('role'); + const type = interaction.options.getString('type'); + const value = interaction.options.getInteger('value'); + const stack = interaction.options.getBoolean('stack') ?? true; + + if (role.managed) { + return interaction.reply({ + content: '❌ Cannot use managed roles (bot roles, integration roles) as rewards.', + ephemeral: true + }); + } + + const botMember = interaction.guild.members.me; + if (role.position >= botMember.roles.highest.position) { + return interaction.reply({ + content: '❌ I cannot assign this role. It is higher than or equal to my highest role.', + ephemeral: true + }); + } + + try { + const { error } = await supabase + .from('activity_roles') + .upsert({ + guild_id: guildId, + role_id: role.id, + milestone_type: type, + milestone_value: value, + stack_roles: stack, + created_at: new Date().toISOString() + }, { onConflict: 'guild_id,role_id' }); + + if (error) throw error; + + const typeNames = { + messages: 'Messages', + voice_hours: 'Voice Hours', + daily_streak: 'Daily Streak', + reactions_given: 'Reactions Given', + reactions_received: 'Reactions Received', + commands_used: 'Commands Used' + }; + + const valueDisplay = type === 'voice_hours' ? `${value} hour(s)` : + type === 'daily_streak' ? `${value} day(s)` : + value.toLocaleString(); + + return interaction.reply({ + content: `✅ ${role} will be awarded at **${valueDisplay} ${typeNames[type]}**.\nRole stacking: ${stack ? 'Enabled (keep previous roles)' : 'Disabled (replace previous roles)'}`, + ephemeral: true + }); + } catch (error) { + console.error('Failed to add activity role:', error.message); + return interaction.reply({ + content: '❌ Failed to add activity role reward. Please try again.', + ephemeral: true + }); + } +} + +async function handleRemove(interaction, supabase, guildId) { + const role = interaction.options.getRole('role'); + + try { + const { data, error } = await supabase + .from('activity_roles') + .delete() + .eq('guild_id', guildId) + .eq('role_id', role.id) + .select(); + + if (error) throw error; + + if (!data || data.length === 0) { + return interaction.reply({ + content: `❌ ${role} is not configured as an activity role reward.`, + ephemeral: true + }); + } + + return interaction.reply({ + content: `✅ Removed ${role} from activity role rewards.`, + ephemeral: true + }); + } catch (error) { + console.error('Failed to remove activity role:', error.message); + return interaction.reply({ + content: '❌ Failed to remove activity role reward. Please try again.', + ephemeral: true + }); + } +} + +async function handleList(interaction, supabase, guildId) { + try { + const { data: roles, error } = await supabase + .from('activity_roles') + .select('*') + .eq('guild_id', guildId) + .order('milestone_type') + .order('milestone_value', { ascending: true }); + + if (error) throw error; + + if (!roles || roles.length === 0) { + return interaction.reply({ + content: '📋 No activity role rewards configured. Use `/activity-roles add` to create some!', + ephemeral: true + }); + } + + const messageRoles = roles.filter(r => r.milestone_type === 'messages'); + const voiceRoles = roles.filter(r => r.milestone_type === 'voice_hours'); + const streakRoles = roles.filter(r => r.milestone_type === 'daily_streak'); + const reactGivenRoles = roles.filter(r => r.milestone_type === 'reactions_given'); + const reactReceivedRoles = roles.filter(r => r.milestone_type === 'reactions_received'); + const commandRoles = roles.filter(r => r.milestone_type === 'commands_used'); + + const formatRoles = (roleList, suffix) => { + if (roleList.length === 0) return 'None configured'; + return roleList.map(r => { + const stackIcon = r.stack_roles ? '📚' : '🔄'; + return `${stackIcon} <@&${r.role_id}> → ${r.milestone_value.toLocaleString()}${suffix}`; + }).join('\n'); + }; + + const embed = new EmbedBuilder() + .setTitle('🎯 Activity Role Rewards') + .setColor(0x10B981) + .setDescription('Roles awarded automatically when users reach activity milestones.') + .addFields( + { name: '💬 Message Roles', value: formatRoles(messageRoles, ' messages'), inline: false }, + { name: '🎤 Voice Hour Roles', value: formatRoles(voiceRoles, ' hours'), inline: false }, + { name: '🔥 Daily Streak Roles', value: formatRoles(streakRoles, ' day streak'), inline: false } + ) + .setFooter({ text: '📚 = Stack roles | 🔄 = Replace previous' }) + .setTimestamp(); + + if (reactGivenRoles.length > 0 || reactReceivedRoles.length > 0 || commandRoles.length > 0) { + embed.addFields( + { name: '👍 Reactions Given Roles', value: formatRoles(reactGivenRoles, ' reactions'), inline: false }, + { name: '❤️ Reactions Received Roles', value: formatRoles(reactReceivedRoles, ' reactions'), inline: false }, + { name: '⚡ Command Usage Roles', value: formatRoles(commandRoles, ' commands'), inline: false } + ); + } + + return interaction.reply({ embeds: [embed] }); + } catch (error) { + console.error('Failed to list activity roles:', error.message); + return interaction.reply({ + content: '❌ Failed to fetch activity role rewards. Please try again.', + ephemeral: true + }); + } +} + +async function handleClear(interaction, supabase, guildId) { + const type = interaction.options.getString('type'); + + try { + let query = supabase.from('activity_roles').delete().eq('guild_id', guildId); + + if (type !== 'all') { + query = query.eq('milestone_type', type); + } + + const { data, error } = await query.select(); + + if (error) throw error; + + const count = data?.length || 0; + const typeNames = { + messages: 'message', + voice_hours: 'voice hour', + daily_streak: 'daily streak', + reactions_given: 'reactions given', + reactions_received: 'reactions received', + commands_used: 'command usage', + all: 'activity' + }; + + return interaction.reply({ + content: `✅ Cleared **${count}** ${typeNames[type]} role reward(s).`, + ephemeral: true + }); + } catch (error) { + console.error('Failed to clear activity roles:', error.message); + return interaction.reply({ + content: '❌ Failed to clear activity role rewards. Please try again.', + ephemeral: true + }); + } +} diff --git a/aethex-bot/public/dashboard.html b/aethex-bot/public/dashboard.html index 4ebd4aa..85a4fcb 100644 --- a/aethex-bot/public/dashboard.html +++ b/aethex-bot/public/dashboard.html @@ -1183,6 +1183,9 @@
+ @@ -1991,6 +1994,67 @@ + +