diff --git a/aethex-bot/commands/federation.js b/aethex-bot/commands/federation.js index 6a575c2..ed3b731 100644 --- a/aethex-bot/commands/federation.js +++ b/aethex-bot/commands/federation.js @@ -1,5 +1,6 @@ const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const { getServerMode, EMBED_COLORS } = require('../utils/modeHelper'); +const { getTrustLevelInfo, getProgressToNextLevel, TRUST_LEVELS, calculateReputationChange } = require('../utils/trustLevels'); module.exports = { data: new SlashCommandBuilder() @@ -60,6 +61,7 @@ module.exports = { ).setRequired(true)) .addStringOption(opt => opt.setName('description').setDescription('Brief description of your server').setRequired(true))) .addSubcommand(sub => sub.setName('status').setDescription('Check your server\'s federation status')) + .addSubcommand(sub => sub.setName('stats').setDescription('View detailed trust level stats and progression')) .addSubcommand(sub => sub.setName('treaty').setDescription('View the Federation Treaty')) ) .addSubcommandGroup(group => @@ -270,6 +272,25 @@ async function handleBans(interaction, supabase, client, subcommand) { return interaction.reply({ content: 'Failed to add ban.', ephemeral: true }); } + const reputationGain = calculateReputationChange('ban_report', true); + + const { data: currentServer } = await supabase + .from('federation_servers') + .select('reports_submitted, reputation_score') + .eq('guild_id', interaction.guildId) + .maybeSingle(); + + if (currentServer) { + await supabase + .from('federation_servers') + .update({ + reports_submitted: (currentServer.reports_submitted || 0) + 1, + reputation_score: (currentServer.reputation_score || 0) + reputationGain, + last_activity: new Date().toISOString() + }) + .eq('guild_id', interaction.guildId); + } + const severityColors = { low: 0xffff00, medium: 0xff9900, high: 0xff3300, critical: 0xff0000 }; const severityEmojis = { low: '⚠️', medium: '🔶', high: '🔴', critical: '☠️' }; @@ -280,7 +301,8 @@ async function handleBans(interaction, supabase, client, subcommand) { .addFields( { name: 'User', value: `${user.tag}\n\`${user.id}\``, inline: true }, { name: 'Severity', value: severity.toUpperCase(), inline: true }, - { name: 'Reason', value: reason } + { name: 'Reason', value: reason }, + { name: 'Reputation', value: `+${reputationGain} points`, inline: true } ) .setFooter({ text: `Added by ${interaction.user.tag}` }) .setTimestamp(); @@ -553,14 +575,20 @@ async function handleMembership(interaction, supabase, client, subcommand) { .maybeSingle(); if (server) { + const trustLevel = server.trust_level || 'bronze'; + const trustInfo = getTrustLevelInfo(trustLevel); + const embed = new EmbedBuilder() - .setColor(0x00ff00) - .setTitle('Federation Member') + .setColor(trustInfo.color) + .setTitle(`${trustInfo.emoji} Federation Member - ${trustInfo.name}`) .addFields( { name: 'Status', value: server.status.toUpperCase(), inline: true }, { name: 'Tier', value: server.tier?.toUpperCase() || 'FREE', inline: true }, + { name: 'Trust Level', value: `${trustInfo.emoji} ${trustInfo.name}`, inline: true }, + { name: 'Reputation', value: `${server.reputation_score || 0} points`, inline: true }, { name: 'Joined', value: new Date(server.joined_at).toLocaleDateString(), inline: true } ) + .setFooter({ text: 'Use /federation membership stats for detailed progression' }) .setTimestamp(); return interaction.reply({ embeds: [embed] }); } @@ -598,6 +626,78 @@ async function handleMembership(interaction, supabase, client, subcommand) { await interaction.reply({ embeds: [embed] }); } + if (subcommand === 'stats') { + const { data: server } = await supabase + .from('federation_servers') + .select('*') + .eq('guild_id', interaction.guildId) + .maybeSingle(); + + if (!server) { + return interaction.reply({ + content: 'This server is not a federation member. Use `/federation membership apply` to join!', + ephemeral: true + }); + } + + const trustLevel = server.trust_level || 'bronze'; + const trustInfo = getTrustLevelInfo(trustLevel); + const progression = getProgressToNextLevel(server); + + const embed = new EmbedBuilder() + .setColor(trustInfo.color) + .setTitle(`${trustInfo.emoji} Trust Level: ${trustInfo.name}`) + .setDescription('Your server\'s federation standing and progression.') + .addFields( + { name: 'Current Benefits', value: [ + `**Auto-Action:** ${trustInfo.benefits.autoAction === 'ban' ? 'Auto-ban threats' : trustInfo.benefits.autoAction === 'kick' ? 'Auto-kick threats' : 'Alert only'}`, + `**Severity Threshold:** ${trustInfo.benefits.severityThreshold.charAt(0).toUpperCase() + trustInfo.benefits.severityThreshold.slice(1)}+`, + `**Featured Eligible:** ${trustInfo.benefits.featuredEligible ? 'Yes' : 'No'}`, + `**Partnership Limit:** ${trustInfo.benefits.partnershipLimit} servers` + ].join('\n') } + ); + + if (progression.nextLevel) { + const progressBars = []; + const makeBar = (pct) => { + const filled = Math.floor(pct / 10); + return '█'.repeat(filled) + '░'.repeat(10 - filled); + }; + + progressBars.push(`**Members:** ${server.member_count || 0}/${progression.progress.members.required} ${progression.progress.members.met ? '✅' : ''}\n\`${makeBar(progression.progress.members.percentage)}\` ${progression.progress.members.percentage}%`); + progressBars.push(`**Days in Federation:** ${progression.progress.days.current}/${progression.progress.days.required} ${progression.progress.days.met ? '✅' : ''}\n\`${makeBar(progression.progress.days.percentage)}\` ${progression.progress.days.percentage}%`); + progressBars.push(`**Reputation:** ${server.reputation_score || 0}/${progression.progress.reputation.required} ${progression.progress.reputation.met ? '✅' : ''}\n\`${makeBar(progression.progress.reputation.percentage)}\` ${progression.progress.reputation.percentage}%`); + + embed.addFields({ + name: `Progress to ${progression.nextLevelInfo.emoji} ${progression.nextLevelInfo.name}`, + value: progressBars.join('\n\n') + }); + + if (progression.allMet) { + embed.addFields({ + name: '🎉 Ready for Upgrade!', + value: 'Your server meets all requirements for the next trust level! Upgrades are processed weekly.' + }); + } + } else { + embed.addFields({ + name: '👑 Maximum Level', + value: 'Your server has reached the highest trust level!' + }); + } + + embed.addFields( + { name: 'Statistics', value: [ + `**Reports Submitted:** ${server.reports_submitted || 0}`, + `**False Positives:** ${server.false_positives || 0}`, + `**Last Activity:** ${server.last_activity ? new Date(server.last_activity).toLocaleDateString() : 'N/A'}` + ].join('\n'), inline: true } + ); + + embed.setTimestamp(); + return interaction.reply({ embeds: [embed] }); + } + if (subcommand === 'treaty') { const embed = new EmbedBuilder() .setColor(0x7c3aed) diff --git a/aethex-bot/listeners/federationProtection.js b/aethex-bot/listeners/federationProtection.js index 04413e4..021a2b6 100644 --- a/aethex-bot/listeners/federationProtection.js +++ b/aethex-bot/listeners/federationProtection.js @@ -1,4 +1,5 @@ const { EmbedBuilder } = require('discord.js'); +const { getTrustLevelInfo, shouldAutoAction } = require('../utils/trustLevels'); module.exports = { name: 'guildMemberAdd', @@ -9,7 +10,7 @@ module.exports = { try { const { data: serverConfig } = await supabase .from('federation_servers') - .select('tier, status') + .select('tier, status, trust_level, reputation_score') .eq('guild_id', member.guild.id) .eq('status', 'approved') .maybeSingle(); @@ -25,10 +26,13 @@ module.exports = { if (!ban) return; + const trustLevel = serverConfig.trust_level || 'bronze'; const isPremium = serverConfig.tier === 'premium'; - const isCritical = ban.severity === 'critical'; + const trustInfo = getTrustLevelInfo(trustLevel); - if (!isPremium && !isCritical) { + const actionDecision = shouldAutoAction(trustLevel, ban.severity, isPremium); + + if (actionDecision.action === 'alert' && ban.severity !== 'critical') { return; } @@ -42,6 +46,7 @@ module.exports = { .addFields( { name: 'User', value: `${member.user.tag}\n\`${member.id}\``, inline: true }, { name: 'Severity', value: ban.severity.toUpperCase(), inline: true }, + { name: 'Trust Level', value: `${trustInfo.emoji} ${trustInfo.name}`, inline: true }, { name: 'Reason', value: ban.reason || 'No reason provided' } ) .setFooter({ text: 'AeThex Federation Protection' }) @@ -60,12 +65,12 @@ module.exports = { } } - if (isCritical) { + if (actionDecision.action === 'ban') { try { - await member.ban({ reason: `[Federation] Global ban: ${ban.reason}` }); + await member.ban({ reason: `[Federation] Global ban (${ban.severity}): ${ban.reason}` }); - alertEmbed.setTitle(`${severityEmojis[ban.severity]} Federation Auto-Ban: Critical Threat Removed`); - alertEmbed.addFields({ name: 'Action Taken', value: 'User was automatically banned' }); + alertEmbed.setTitle(`${severityEmojis[ban.severity]} Federation Auto-Ban: Threat Removed`); + alertEmbed.addFields({ name: 'Action Taken', value: `User was automatically banned (${trustInfo.name} Protection)` }); await supabase.from('federation_alerts').update({ delivered: true, @@ -77,10 +82,10 @@ module.exports = { console.error('[Federation] Failed to auto-ban:', banError.message); alertEmbed.addFields({ name: 'Action Required', value: 'Auto-ban failed. Please ban manually.' }); } - } else if (isPremium) { + } else if (actionDecision.action === 'kick') { try { await member.kick(`[Federation] Global ban (${ban.severity} severity): ${ban.reason}`); - alertEmbed.addFields({ name: 'Action Taken', value: 'User was automatically kicked (Premium Protection)' }); + alertEmbed.addFields({ name: 'Action Taken', value: `User was automatically kicked (${trustInfo.name} Protection)` }); await supabase.from('federation_alerts').update({ delivered: true, @@ -93,6 +98,11 @@ module.exports = { } } + await supabase + .from('federation_servers') + .update({ last_activity: new Date().toISOString() }) + .eq('guild_id', member.guild.id); + const owner = await member.guild.fetchOwner().catch(() => null); if (owner && ban.severity === 'critical') { try { diff --git a/aethex-bot/server/webServer.js b/aethex-bot/server/webServer.js index bff73ad..8252e16 100644 --- a/aethex-bot/server/webServer.js +++ b/aethex-bot/server/webServer.js @@ -422,6 +422,90 @@ function createWebServer(discordClient, supabase, options = {}) { } }); + // Verification success endpoint - called by aethex.dev when verification completes + app.post('/api/verify-success', async (req, res) => { + if (!supabase) { + return res.status(503).json({ error: 'Database not available' }); + } + + const webhookSecret = process.env.DISCORD_BOT_WEBHOOK_SECRET; + const providedSecret = req.headers['x-webhook-secret'] || req.body.secret; + + if (webhookSecret && providedSecret !== webhookSecret) { + console.log('[Verify-Success] Invalid webhook secret provided'); + return res.status(401).json({ error: 'Invalid webhook secret' }); + } + + const { discord_id, user_id, username } = req.body; + + if (!discord_id) { + return res.status(400).json({ error: 'Missing discord_id' }); + } + + console.log(`[Verify-Success] Received verification for Discord ID: ${discord_id}`); + + try { + // Update or create the discord link + if (user_id) { + await supabase.from('discord_links').upsert({ + discord_id, + user_id, + username: username || null, + linked_at: new Date().toISOString() + }, { onConflict: 'discord_id' }); + } + + // Get server configs that have a verified role configured + const { data: configs } = await supabase + .from('server_config') + .select('guild_id, verified_role_id') + .not('verified_role_id', 'is', null); + + let rolesAssigned = 0; + const assignedIn = []; + + // Assign verified role in all guilds where the user is a member + for (const config of configs || []) { + if (!config.verified_role_id) continue; + + try { + const guild = client.guilds.cache.get(config.guild_id); + if (!guild) continue; + + const member = await guild.members.fetch(discord_id).catch(() => null); + if (!member) continue; + + const role = guild.roles.cache.get(config.verified_role_id); + if (!role) continue; + + if (!member.roles.cache.has(role.id)) { + await member.roles.add(role, 'Account verified via aethex.dev'); + rolesAssigned++; + assignedIn.push(guild.name); + console.log(`[Verify-Success] Assigned verified role to ${member.user.tag} in ${guild.name}`); + } + } catch (err) { + console.error(`[Verify-Success] Failed to assign role in guild ${config.guild_id}:`, err.message); + } + } + + // Clean up any pending verification codes + await supabase.from('discord_verifications').delete().eq('discord_id', discord_id); + + res.json({ + success: true, + rolesAssigned, + assignedIn, + message: rolesAssigned > 0 + ? `Verified role assigned in ${rolesAssigned} server(s)` + : 'Verification recorded (user not found in any configured servers)' + }); + } catch (error) { + console.error('[Verify-Success] Error:', error); + res.status(500).json({ error: 'Failed to process verification' }); + } + }); + app.get('/api/stats/:userId/:guildId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); diff --git a/aethex-bot/utils/trustLevels.js b/aethex-bot/utils/trustLevels.js new file mode 100644 index 0000000..a829afd --- /dev/null +++ b/aethex-bot/utils/trustLevels.js @@ -0,0 +1,195 @@ +const TRUST_LEVELS = { + bronze: { + name: 'Bronze', + emoji: '🥉', + color: 0xcd7f32, + requirements: { + minMembers: 0, + minDays: 0, + minReputationScore: 0 + }, + benefits: { + autoAction: 'alert', + severityThreshold: 'critical', + directoryListing: true, + featuredEligible: false, + partnershipLimit: 3 + } + }, + silver: { + name: 'Silver', + emoji: '🥈', + color: 0xc0c0c0, + requirements: { + minMembers: 100, + minDays: 30, + minReputationScore: 50 + }, + benefits: { + autoAction: 'kick', + severityThreshold: 'medium', + directoryListing: true, + featuredEligible: false, + partnershipLimit: 5 + } + }, + gold: { + name: 'Gold', + emoji: '🥇', + color: 0xffd700, + requirements: { + minMembers: 500, + minDays: 90, + minReputationScore: 200 + }, + benefits: { + autoAction: 'ban', + severityThreshold: 'medium', + directoryListing: true, + featuredEligible: true, + partnershipLimit: 10 + } + }, + platinum: { + name: 'Platinum', + emoji: '💎', + color: 0xe5e4e2, + requirements: { + minMembers: 1000, + minDays: 180, + minReputationScore: 500 + }, + benefits: { + autoAction: 'ban', + severityThreshold: 'low', + directoryListing: true, + featuredEligible: true, + partnershipLimit: 20 + } + } +}; + +const TRUST_ORDER = ['bronze', 'silver', 'gold', 'platinum']; + +function calculateTrustLevel(serverData) { + const memberCount = serverData.member_count || 0; + const joinedAt = new Date(serverData.joined_federation_at || serverData.joined_at); + const daysInFederation = Math.floor((Date.now() - joinedAt.getTime()) / (1000 * 60 * 60 * 24)); + const reputationScore = serverData.reputation_score || 0; + + let qualifiedLevel = 'bronze'; + + for (const level of TRUST_ORDER) { + const reqs = TRUST_LEVELS[level].requirements; + if (memberCount >= reqs.minMembers && + daysInFederation >= reqs.minDays && + reputationScore >= reqs.minReputationScore) { + qualifiedLevel = level; + } else { + break; + } + } + + return qualifiedLevel; +} + +function getTrustLevelInfo(level) { + return TRUST_LEVELS[level] || TRUST_LEVELS.bronze; +} + +function getNextTrustLevel(currentLevel) { + const currentIndex = TRUST_ORDER.indexOf(currentLevel); + if (currentIndex === -1 || currentIndex >= TRUST_ORDER.length - 1) { + return null; + } + return TRUST_ORDER[currentIndex + 1]; +} + +function getProgressToNextLevel(serverData) { + const currentLevel = serverData.trust_level || 'bronze'; + const nextLevel = getNextTrustLevel(currentLevel); + + if (!nextLevel) { + return { nextLevel: null, progress: {}, allMet: true }; + } + + const reqs = TRUST_LEVELS[nextLevel].requirements; + const memberCount = serverData.member_count || 0; + const joinedAt = new Date(serverData.joined_federation_at || serverData.joined_at); + const daysInFederation = Math.floor((Date.now() - joinedAt.getTime()) / (1000 * 60 * 60 * 24)); + const reputationScore = serverData.reputation_score || 0; + + const progress = { + members: { + current: memberCount, + required: reqs.minMembers, + met: memberCount >= reqs.minMembers, + percentage: Math.min(100, Math.floor((memberCount / reqs.minMembers) * 100)) || 0 + }, + days: { + current: daysInFederation, + required: reqs.minDays, + met: daysInFederation >= reqs.minDays, + percentage: Math.min(100, Math.floor((daysInFederation / reqs.minDays) * 100)) || 0 + }, + reputation: { + current: reputationScore, + required: reqs.minReputationScore, + met: reputationScore >= reqs.minReputationScore, + percentage: Math.min(100, Math.floor((reputationScore / reqs.minReputationScore) * 100)) || 0 + } + }; + + const allMet = progress.members.met && progress.days.met && progress.reputation.met; + + return { nextLevel, nextLevelInfo: TRUST_LEVELS[nextLevel], progress, allMet }; +} + +function shouldAutoAction(trustLevel, banSeverity, isPremium) { + const levelInfo = getTrustLevelInfo(trustLevel); + const severityOrder = ['low', 'medium', 'high', 'critical']; + + const banSeverityIndex = severityOrder.indexOf(banSeverity); + const thresholdIndex = severityOrder.indexOf(levelInfo.benefits.severityThreshold); + + if (banSeverityIndex < thresholdIndex) { + if (banSeverity === 'critical') { + return { action: 'ban', reason: 'critical_severity' }; + } + return { action: 'alert', reason: 'below_threshold' }; + } + + if (isPremium) { + return { action: levelInfo.benefits.autoAction, reason: 'premium_protection' }; + } + + if (banSeverity === 'critical') { + return { action: 'ban', reason: 'critical_severity' }; + } + + return { action: 'alert', reason: 'free_tier' }; +} + +function calculateReputationChange(action, successful = true) { + const reputationValues = { + ban_report: successful ? 10 : -5, + report_verified: 15, + false_positive: -20, + event_hosted: 25, + partnership_formed: 10, + monthly_activity: 5 + }; + + return reputationValues[action] || 0; +} + +module.exports = { + TRUST_LEVELS, + TRUST_ORDER, + calculateTrustLevel, + getTrustLevelInfo, + getNextTrustLevel, + getProgressToNextLevel, + shouldAutoAction, + calculateReputationChange +};