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
This commit is contained in:
parent
ede721af0f
commit
3e26b99d65
3 changed files with 667 additions and 0 deletions
282
aethex-bot/commands/activity-roles.js
Normal file
282
aethex-bot/commands/activity-roles.js
Normal file
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1183,6 +1183,9 @@
|
|||
<div class="nav-item" data-page="analytics">
|
||||
<span class="nav-icon">📊</span> Analytics
|
||||
</div>
|
||||
<div class="nav-item" data-page="activity-roles">
|
||||
<span class="nav-icon">🎯</span> Activity Roles
|
||||
</div>
|
||||
<div class="nav-item" data-page="federation">
|
||||
<span class="nav-icon">🌐</span> Federation
|
||||
</div>
|
||||
|
|
@ -1991,6 +1994,67 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-activity-roles" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title"><span class="text-gradient">Activity</span> Roles</h1>
|
||||
<p class="page-subtitle">Automatically award roles when users reach activity milestones</p>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:1.5rem">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Add Activity Role</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>Role to Award</label>
|
||||
<select id="activityRoleSelect" class="form-input">
|
||||
<option value="">Select a role...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Milestone Type</label>
|
||||
<select id="activityMilestoneType" class="form-input">
|
||||
<option value="messages">Messages Sent</option>
|
||||
<option value="voice_hours">Voice Hours</option>
|
||||
<option value="daily_streak">Daily Streak (Days)</option>
|
||||
<option value="reactions_given">Reactions Given</option>
|
||||
<option value="reactions_received">Reactions Received</option>
|
||||
<option value="commands_used">Commands Used</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Milestone Value</label>
|
||||
<input type="number" id="activityMilestoneValue" class="form-input" min="1" placeholder="e.g. 100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Stack Roles</label>
|
||||
<select id="activityStackRoles" class="form-input">
|
||||
<option value="true">Yes - Keep previous roles</option>
|
||||
<option value="false">No - Replace previous roles</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:1rem">
|
||||
<button class="btn btn-primary" onclick="addActivityRole()">Add Activity Role</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Configured Activity Roles</h3>
|
||||
</div>
|
||||
<div class="card-body" id="activityRolesList">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🎯</div>
|
||||
<p>No activity roles configured</p>
|
||||
<p style="font-size:0.875rem;color:var(--muted);margin-top:0.5rem">Add roles above to automatically reward active members</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-federation" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title"><span class="text-gradient">Federation</span> Management</h1>
|
||||
|
|
@ -2280,6 +2344,7 @@
|
|||
case 'federation': loadFederationData(); break;
|
||||
case 'moderation': loadModerationData(); break;
|
||||
case 'analytics': loadAnalyticsData(); break;
|
||||
case 'activity-roles': loadActivityRoles(); break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3675,6 +3740,147 @@
|
|||
return guild?.isAdmin === true;
|
||||
}
|
||||
|
||||
// Activity Roles Functions
|
||||
async function loadActivityRoles() {
|
||||
if (!currentGuild) return;
|
||||
|
||||
await loadGuildRoles();
|
||||
|
||||
const container = document.getElementById('activityRolesList');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/guild/' + currentGuild + '/activity-roles');
|
||||
if (!res.ok) throw new Error('Failed to load');
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.roles || data.roles.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🎯</div>
|
||||
<p>No activity roles configured</p>
|
||||
<p style="font-size:0.875rem;color:var(--muted);margin-top:0.5rem">Add roles above to automatically reward active members</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const typeLabels = {
|
||||
messages: 'Messages',
|
||||
voice_hours: 'Voice Hours',
|
||||
daily_streak: 'Daily Streak',
|
||||
reactions_given: 'Reactions Given',
|
||||
reactions_received: 'Reactions Received',
|
||||
commands_used: 'Commands Used'
|
||||
};
|
||||
|
||||
const typeSuffix = {
|
||||
messages: ' messages',
|
||||
voice_hours: ' hours',
|
||||
daily_streak: ' days',
|
||||
reactions_given: ' reactions',
|
||||
reactions_received: ' reactions',
|
||||
commands_used: ' commands'
|
||||
};
|
||||
|
||||
container.innerHTML = data.roles.map(r => `
|
||||
<div class="item-row">
|
||||
<div style="width:40px;height:40px;background:${r.role_color || '#6366f1'};border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.25rem">🎯</div>
|
||||
<div class="item-info">
|
||||
<div class="item-name">
|
||||
${escapeHtml(r.role_name)}
|
||||
<span class="item-badge ${r.stack_roles ? 'active' : 'inactive'}">${r.stack_roles ? 'Stacking' : 'Replace'}</span>
|
||||
</div>
|
||||
<div class="item-desc">${typeLabels[r.milestone_type] || r.milestone_type}: ${r.milestone_value.toLocaleString()}${typeSuffix[r.milestone_type] || ''}</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="btn-icon danger" onclick="deleteActivityRole('${r.role_id}')" title="Delete">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
container.innerHTML = '<div class="empty-state">Failed to load activity roles</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGuildRoles() {
|
||||
if (!currentGuild) return;
|
||||
|
||||
const select = document.getElementById('activityRoleSelect');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/guild/' + currentGuild + '/roles');
|
||||
if (!res.ok) throw new Error('Failed to load');
|
||||
const data = await res.json();
|
||||
|
||||
select.innerHTML = '<option value="">Select a role...</option>' +
|
||||
(data.roles || []).map(r => `<option value="${r.id}" style="color:${r.color}">${escapeHtml(r.name)}</option>`).join('');
|
||||
} catch (e) {
|
||||
select.innerHTML = '<option value="">Failed to load roles</option>';
|
||||
}
|
||||
}
|
||||
|
||||
async function addActivityRole() {
|
||||
if (!currentGuild) return;
|
||||
|
||||
const roleId = document.getElementById('activityRoleSelect').value;
|
||||
const milestoneType = document.getElementById('activityMilestoneType').value;
|
||||
const milestoneValue = parseInt(document.getElementById('activityMilestoneValue').value);
|
||||
const stackRoles = document.getElementById('activityStackRoles').value === 'true';
|
||||
|
||||
if (!roleId) {
|
||||
alert('Please select a role');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!milestoneValue || milestoneValue < 1) {
|
||||
alert('Please enter a valid milestone value');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/guild/' + currentGuild + '/activity-roles', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
role_id: roleId,
|
||||
milestone_type: milestoneType,
|
||||
milestone_value: milestoneValue,
|
||||
stack_roles: stackRoles
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
document.getElementById('activityRoleSelect').value = '';
|
||||
document.getElementById('activityMilestoneValue').value = '';
|
||||
await loadActivityRoles();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert(err.error || 'Failed to add activity role');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to add activity role');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteActivityRole(roleId) {
|
||||
if (!confirm('Remove this activity role?')) return;
|
||||
if (!currentGuild) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/guild/' + currentGuild + '/activity-roles/' + roleId, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
await loadActivityRoles();
|
||||
} else {
|
||||
alert('Failed to delete activity role');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to delete activity role');
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -2090,6 +2090,185 @@ function createWebServer(discordClient, supabase, options = {}) {
|
|||
}
|
||||
});
|
||||
|
||||
// Activity Roles API endpoints
|
||||
app.get('/api/guild/:guildId/activity-roles', async (req, res) => {
|
||||
if (!supabase) return res.status(503).json({ error: 'Database not available' });
|
||||
|
||||
const userId = req.session.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
|
||||
|
||||
const { guildId } = req.params;
|
||||
|
||||
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
||||
if (!userGuild || !userGuild.isAdmin) {
|
||||
return res.status(403).json({ error: 'No admin access' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: roles } = await supabase
|
||||
.from('activity_roles')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId)
|
||||
.order('milestone_type')
|
||||
.order('milestone_value', { ascending: true });
|
||||
|
||||
// Get Discord role names from the bot
|
||||
let guild = discordClient.guilds.cache.get(guildId);
|
||||
|
||||
// Try to fetch guild if not in cache
|
||||
if (!guild) {
|
||||
try {
|
||||
guild = await discordClient.guilds.fetch(guildId);
|
||||
if (guild && !guild.roles.cache.size) {
|
||||
await guild.roles.fetch();
|
||||
}
|
||||
} catch (fetchErr) {
|
||||
// Guild not accessible, continue with unknown role names
|
||||
console.log(`Guild ${guildId} not accessible for role names`);
|
||||
}
|
||||
}
|
||||
|
||||
const rolesWithNames = (roles || []).map(r => {
|
||||
const discordRole = guild?.roles.cache.get(r.role_id);
|
||||
return {
|
||||
...r,
|
||||
role_name: discordRole?.name || `Unknown (${r.role_id})`,
|
||||
role_color: discordRole?.hexColor || '#99aab5'
|
||||
};
|
||||
});
|
||||
|
||||
res.json({ roles: rolesWithNames });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch activity roles:', error);
|
||||
res.status(500).json({ error: 'Failed to load activity roles' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/guild/:guildId/activity-roles', async (req, res) => {
|
||||
if (!supabase) return res.status(503).json({ error: 'Database not available' });
|
||||
|
||||
const userId = req.session.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
|
||||
|
||||
const { guildId } = req.params;
|
||||
|
||||
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
||||
if (!userGuild || !userGuild.isAdmin) {
|
||||
return res.status(403).json({ error: 'No admin access' });
|
||||
}
|
||||
|
||||
const { role_id, milestone_type, milestone_value, stack_roles } = req.body;
|
||||
|
||||
if (!role_id || !milestone_type || milestone_value === undefined || milestone_value === null) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
const validTypes = ['messages', 'voice_hours', 'daily_streak', 'reactions_given', 'reactions_received', 'commands_used'];
|
||||
if (!validTypes.includes(milestone_type)) {
|
||||
return res.status(400).json({ error: 'Invalid milestone type' });
|
||||
}
|
||||
|
||||
const parsedValue = parseInt(milestone_value, 10);
|
||||
if (isNaN(parsedValue) || parsedValue < 1) {
|
||||
return res.status(400).json({ error: 'Milestone value must be a positive integer (1 or greater)' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('activity_roles')
|
||||
.upsert({
|
||||
guild_id: guildId,
|
||||
role_id,
|
||||
milestone_type,
|
||||
milestone_value: parsedValue,
|
||||
stack_roles: stack_roles !== false,
|
||||
created_at: new Date().toISOString()
|
||||
}, { onConflict: 'guild_id,role_id' });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to add activity role:', error);
|
||||
res.status(500).json({ error: 'Failed to add activity role' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/guild/:guildId/activity-roles/:roleId', async (req, res) => {
|
||||
if (!supabase) return res.status(503).json({ error: 'Database not available' });
|
||||
|
||||
const userId = req.session.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
|
||||
|
||||
const { guildId, roleId } = req.params;
|
||||
|
||||
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
||||
if (!userGuild || !userGuild.isAdmin) {
|
||||
return res.status(403).json({ error: 'No admin access' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('activity_roles')
|
||||
.delete()
|
||||
.eq('guild_id', guildId)
|
||||
.eq('role_id', roleId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to delete activity role:', error);
|
||||
res.status(500).json({ error: 'Failed to delete activity role' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/guild/:guildId/roles', async (req, res) => {
|
||||
const userId = req.session.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
|
||||
|
||||
const { guildId } = req.params;
|
||||
|
||||
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
||||
if (!userGuild || !userGuild.isAdmin) {
|
||||
return res.status(403).json({ error: 'No admin access' });
|
||||
}
|
||||
|
||||
try {
|
||||
let guild = discordClient.guilds.cache.get(guildId);
|
||||
|
||||
// Try to fetch guild if not in cache
|
||||
if (!guild) {
|
||||
try {
|
||||
guild = await discordClient.guilds.fetch(guildId);
|
||||
} catch (fetchErr) {
|
||||
console.log(`Guild ${guildId} not accessible by bot`);
|
||||
return res.json({ roles: [], message: 'Bot is not in this server or cannot access it' });
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure roles are cached
|
||||
if (!guild.roles.cache.size) {
|
||||
await guild.roles.fetch();
|
||||
}
|
||||
|
||||
const roles = guild.roles.cache
|
||||
.filter(r => !r.managed && r.id !== guild.id)
|
||||
.sort((a, b) => b.position - a.position)
|
||||
.map(r => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
color: r.hexColor,
|
||||
position: r.position
|
||||
}));
|
||||
|
||||
res.json({ roles });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch roles:', error);
|
||||
res.status(500).json({ error: 'Failed to load roles' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
|
|
|
|||
Loading…
Reference in a new issue