AeThex-Bot-Master/aethex-bot/commands/activity-roles.js
sirpiglr 3e26b99d65 Add functionality to automatically award roles based on user activity
Adds new API endpoints and frontend components for managing automatic role rewards based on various user activity milestones, including validation for milestone values and improved handling of guild data.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 2417c2c2-9cf1-48e0-b7b0-da47f0f0530a
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/bakeZwZ
Replit-Helium-Checkpoint-Created: true
2025-12-12 23:46:17 +00:00

282 lines
10 KiB
JavaScript

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
});
}
}