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:
parent
a1912fc48a
commit
c7c4705c52
3 changed files with 313 additions and 0 deletions
|
|
@ -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
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue