Add a daily scheduler to evaluate and update federation trust levels

Introduces a new daily scheduler in `bot.js` to evaluate federation server trust levels based on member count and other factors. Includes an API endpoint `/api/federation/guild/:guildId` in `webServer.js` to fetch guild-specific federation status, and updates `dashboard.html` to display this information.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 342196c8-6efc-4cab-9367-2e90abd6f3e1
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/HdH3K6u
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-13 10:52:52 +00:00
parent a1912fc48a
commit c7c4705c52
3 changed files with 313 additions and 0 deletions

View file

@ -2781,6 +2781,7 @@ client.once("clientReady", async () => {
// Start automatic backup scheduler
if (supabase) {
startAutoBackupScheduler(client, supabase);
startFederationTrustEvaluator(client, supabase);
}
});
@ -2863,6 +2864,124 @@ async function startAutoBackupScheduler(discordClient, supabaseClient) {
console.log('[Backup] Scheduler started - checking every hour');
}
// =============================================================================
// FEDERATION TRUST LEVEL PROGRESSION SCHEDULER
// =============================================================================
async function startFederationTrustEvaluator(discordClient, supabaseClient) {
console.log('[Federation] Starting trust level evaluation scheduler...');
const { calculateTrustLevel, getTrustLevelInfo } = require('./utils/trustLevels');
// Evaluate trust levels daily
setInterval(async () => {
try {
const { data: servers } = await supabaseClient
.from('federation_servers')
.select('*')
.eq('status', 'approved');
if (!servers || servers.length === 0) return;
let upgrades = 0;
let downgrades = 0;
for (const server of servers) {
// Get current member count from Discord
const guild = discordClient.guilds.cache.get(server.guild_id);
const memberCount = guild?.memberCount || server.member_count || 0;
// Update member count in database
if (guild && memberCount !== server.member_count) {
await supabaseClient
.from('federation_servers')
.update({ member_count: memberCount })
.eq('guild_id', server.guild_id);
}
// Calculate new trust level
const serverData = { ...server, member_count: memberCount };
const newLevel = calculateTrustLevel(serverData);
const currentLevel = server.trust_level || 'bronze';
if (newLevel !== currentLevel) {
const oldInfo = getTrustLevelInfo(currentLevel);
const newInfo = getTrustLevelInfo(newLevel);
await supabaseClient
.from('federation_servers')
.update({
trust_level: newLevel,
updated_at: new Date().toISOString(),
last_activity: new Date().toISOString()
})
.eq('guild_id', server.guild_id);
// Determine if upgrade or downgrade
const levels = ['bronze', 'silver', 'gold', 'platinum'];
const isUpgrade = levels.indexOf(newLevel) > levels.indexOf(currentLevel);
if (isUpgrade) {
upgrades++;
console.log(`[Federation] ${server.guild_name}: ${oldInfo.emoji} ${oldInfo.name}${newInfo.emoji} ${newInfo.name} (UPGRADE)`);
} else {
downgrades++;
console.log(`[Federation] ${server.guild_name}: ${oldInfo.emoji} ${oldInfo.name}${newInfo.emoji} ${newInfo.name} (DOWNGRADE)`);
}
// Send notification to the server if we can
if (guild) {
try {
const systemChannel = guild.systemChannel;
if (systemChannel) {
const { EmbedBuilder } = require('discord.js');
const embed = new EmbedBuilder()
.setColor(newInfo.color)
.setTitle(`${newInfo.emoji} Federation Trust Level ${isUpgrade ? 'Upgrade' : 'Change'}!`)
.setDescription(isUpgrade
? `Congratulations! Your server has been promoted to **${newInfo.name}** tier in the Federation.`
: `Your server's Federation tier has been adjusted to **${newInfo.name}**.`)
.addFields(
{ name: 'Previous Tier', value: `${oldInfo.emoji} ${oldInfo.name}`, inline: true },
{ name: 'New Tier', value: `${newInfo.emoji} ${newInfo.name}`, inline: true }
)
.setTimestamp();
await systemChannel.send({ embeds: [embed] }).catch(() => {});
}
} catch (e) {
// Silent fail for notifications
}
}
}
}
if (upgrades > 0 || downgrades > 0) {
console.log(`[Federation] Trust evaluation complete: ${upgrades} upgrades, ${downgrades} downgrades`);
}
} catch (error) {
console.error('[Federation] Trust evaluation error:', error.message);
}
}, 24 * 60 * 60 * 1000); // Evaluate daily
// Also run immediately on startup (after a short delay)
setTimeout(async () => {
console.log('[Federation] Running initial trust level evaluation...');
try {
const { data: servers } = await supabaseClient
.from('federation_servers')
.select('*')
.eq('status', 'approved');
console.log(`[Federation] Monitoring ${servers?.length || 0} federation servers`);
} catch (e) {
console.error('[Federation] Initial check error:', e.message);
}
}, 10000);
console.log('[Federation] Trust evaluator started - checking daily');
}
// =============================================================================
// ERROR HANDLING
// =============================================================================

View file

@ -2241,6 +2241,19 @@
<p class="page-subtitle">Manage your federation membership, view bans, and applications</p>
</div>
<div id="fedGuildStatus" class="card" style="margin-bottom:1.5rem">
<div class="card-header">
<h3 class="card-title">Your Server Status</h3>
<span id="fedMemberBadge" class="badge" style="display:none">Federation Member</span>
</div>
<div class="card-body" id="fedGuildStatusContent">
<div class="empty-state">
<div class="loading-spinner"></div>
<p style="margin-top:1rem">Loading status...</p>
</div>
</div>
</div>
<div class="stats-grid" id="fedStatsGrid">
<div class="stat-card">
<div class="stat-label">Member Servers</div>
@ -3517,7 +3530,118 @@
}
}
async function loadFederationGuildStatus() {
if (!currentGuild) return;
const container = document.getElementById('fedGuildStatusContent');
const badge = document.getElementById('fedMemberBadge');
try {
const res = await fetch('/api/federation/guild/' + currentGuild);
const data = await res.json();
if (!data.member) {
badge.style.display = 'none';
container.innerHTML = `
<div class="empty-state" style="padding:2rem">
<div style="font-size:2.5rem;margin-bottom:1rem">🌐</div>
<p style="margin-bottom:1rem">This server is not a Federation member.</p>
<p style="color:var(--muted);font-size:0.9rem;margin-bottom:1.5rem">Join the Federation to get cross-server protection from raiders, scammers, and malicious users.</p>
<a href="/federation" class="btn btn-primary">Learn More & Apply</a>
</div>
`;
return;
}
badge.style.display = 'inline-block';
badge.style.background = 'linear-gradient(135deg, var(--primary), #3b82f6)';
badge.style.color = 'white';
badge.style.padding = '0.25rem 0.75rem';
badge.style.borderRadius = '20px';
badge.style.fontSize = '0.75rem';
badge.style.fontWeight = '600';
const tl = data.trustLevel;
const rep = data.reputation;
const prog = data.progression;
let progressHtml = '';
if (prog.nextLevel && prog.progress) {
const p = prog.progress;
progressHtml = `
<div style="margin-top:1.5rem;padding-top:1.5rem;border-top:1px solid var(--border)">
<h4 style="margin-bottom:1rem;font-size:0.95rem">Progress to ${prog.nextLevelInfo?.emoji || ''} ${prog.nextLevelInfo?.name || prog.nextLevel}</h4>
<div style="display:grid;gap:1rem">
<div>
<div style="display:flex;justify-content:space-between;font-size:0.85rem;margin-bottom:0.25rem">
<span>Members</span>
<span style="color:${p.members.met ? 'var(--success)' : 'var(--muted)'}">${p.members.current.toLocaleString()} / ${p.members.required.toLocaleString()} ${p.members.met ? '✓' : ''}</span>
</div>
<div class="progress-bar"><div class="progress-fill" style="width:${p.members.percentage}%;background:${p.members.met ? 'var(--success)' : 'linear-gradient(90deg,var(--primary),#3b82f6)'}"></div></div>
</div>
<div>
<div style="display:flex;justify-content:space-between;font-size:0.85rem;margin-bottom:0.25rem">
<span>Days in Federation</span>
<span style="color:${p.days.met ? 'var(--success)' : 'var(--muted)'}">${p.days.current} / ${p.days.required} ${p.days.met ? '✓' : ''}</span>
</div>
<div class="progress-bar"><div class="progress-fill" style="width:${p.days.percentage}%;background:${p.days.met ? 'var(--success)' : 'linear-gradient(90deg,var(--primary),#3b82f6)'}"></div></div>
</div>
<div>
<div style="display:flex;justify-content:space-between;font-size:0.85rem;margin-bottom:0.25rem">
<span>Reputation Score</span>
<span style="color:${p.reputation.met ? 'var(--success)' : 'var(--muted)'}">${p.reputation.current} / ${p.reputation.required} ${p.reputation.met ? '✓' : ''}</span>
</div>
<div class="progress-bar"><div class="progress-fill" style="width:${p.reputation.percentage}%;background:${p.reputation.met ? 'var(--success)' : 'linear-gradient(90deg,var(--primary),#3b82f6)'}"></div></div>
</div>
</div>
${prog.allMet ? '<p style="margin-top:1rem;color:var(--success);font-size:0.9rem">✨ All requirements met! Upgrade will be applied in the next evaluation.</p>' : ''}
</div>
`;
} else {
progressHtml = '<p style="margin-top:1.5rem;padding-top:1.5rem;border-top:1px solid var(--border);color:var(--success);font-size:0.9rem">🎉 Maximum trust level reached!</p>';
}
container.innerHTML = `
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1.5rem">
<div style="text-align:center;padding:1.5rem;background:rgba(99,102,241,0.08);border-radius:16px;border:1px solid rgba(99,102,241,0.2)">
<div style="font-size:3rem;margin-bottom:0.5rem">${tl.emoji}</div>
<div style="font-size:1.25rem;font-weight:600;margin-bottom:0.25rem">${tl.name} Tier</div>
<div style="font-size:0.85rem;color:var(--muted)">Trust Level</div>
</div>
<div style="padding:1.5rem">
<div style="margin-bottom:1rem">
<div style="font-size:0.85rem;color:var(--muted);margin-bottom:0.25rem">Reputation Score</div>
<div style="font-size:1.5rem;font-weight:700;color:var(--primary)">${rep.score.toLocaleString()}</div>
</div>
<div style="margin-bottom:1rem">
<div style="font-size:0.85rem;color:var(--muted);margin-bottom:0.25rem">Reports Submitted</div>
<div style="font-weight:600">${rep.reports_submitted}</div>
</div>
<div>
<div style="font-size:0.85rem;color:var(--muted);margin-bottom:0.25rem">False Positives</div>
<div style="font-weight:600;color:${rep.false_positives > 0 ? 'var(--warning)' : 'var(--success)'}">${rep.false_positives}</div>
</div>
</div>
<div style="padding:1.5rem;background:var(--card);border-radius:16px;border:1px solid var(--card-border)">
<div style="font-size:0.9rem;font-weight:600;margin-bottom:1rem">Current Benefits</div>
<div style="font-size:0.85rem;color:var(--muted);line-height:1.8">
<div>🛡️ Auto Action: <span style="color:var(--foreground);font-weight:500">${tl.benefits.autoAction.toUpperCase()}</span></div>
<div>⚠️ Severity Threshold: <span style="color:var(--foreground);font-weight:500">${tl.benefits.severityThreshold.toUpperCase()}</span></div>
<div>📋 Directory Listing: <span style="color:var(--foreground);font-weight:500">${tl.benefits.directoryListing ? 'Yes' : 'No'}</span></div>
<div>⭐ Featured Eligible: <span style="color:var(--foreground);font-weight:500">${tl.benefits.featuredEligible ? 'Yes' : 'No'}</span></div>
<div>🤝 Partnership Limit: <span style="color:var(--foreground);font-weight:500">${tl.benefits.partnershipLimit}</span></div>
</div>
</div>
</div>
${progressHtml}
`;
} catch (e) {
console.error('Failed to load federation guild status:', e);
container.innerHTML = '<div class="empty-state">Failed to load federation status</div>';
}
}
function loadFederationData() {
loadFederationGuildStatus();
loadFederationStats();
loadFederationServers();
loadFederationBans();

View file

@ -1497,6 +1497,76 @@ function createWebServer(discordClient, supabase, options = {}) {
}
});
app.get('/api/federation/guild/:guildId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const { guildId } = req.params;
try {
const { data: server } = await supabase
.from('federation_servers')
.select('*')
.eq('guild_id', guildId)
.maybeSingle();
if (!server) {
return res.json({
member: false,
message: 'This server is not a federation member'
});
}
const { getProgressToNextLevel, getTrustLevelInfo, TRUST_LEVELS } = require('../utils/trustLevels');
const trustLevelInfo = getTrustLevelInfo(server.trust_level || 'bronze');
const progression = getProgressToNextLevel(server);
const guild = discordClient.guilds.cache.get(guildId);
const memberCount = guild?.memberCount || server.member_count || 0;
res.json({
member: true,
server: {
guild_id: server.guild_id,
guild_name: server.guild_name,
guild_icon: server.guild_icon,
member_count: memberCount,
tier: server.tier,
status: server.status,
joined_at: server.created_at,
joined_federation_at: server.joined_federation_at || server.created_at
},
trustLevel: {
level: server.trust_level || 'bronze',
name: trustLevelInfo.name,
emoji: trustLevelInfo.emoji,
color: trustLevelInfo.color,
benefits: trustLevelInfo.benefits
},
reputation: {
score: server.reputation_score || 0,
reports_submitted: server.reports_submitted || 0,
false_positives: server.false_positives || 0,
last_activity: server.last_activity
},
progression: {
nextLevel: progression.nextLevel,
nextLevelInfo: progression.nextLevelInfo ? {
name: progression.nextLevelInfo.name,
emoji: progression.nextLevelInfo.emoji
} : null,
progress: progression.progress,
allMet: progression.allMet
}
});
} catch (error) {
console.error('Failed to fetch guild federation stats:', error);
res.status(500).json({ error: 'Failed to fetch federation stats' });
}
});
// ============ STRIPE PAYMENT API ============
app.post('/api/stripe/create-checkout', async (req, res) => {