Enhance bot with federation protection and new command features
Introduces federation protection listener, expands federation commands, adds federation API endpoints to web server, and updates bot status to 'Protecting the Federation'. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: d254ee4b-e69e-44a5-b1be-b89c088a485a Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/FUs0R2K Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
fd734b5dbd
commit
76cad00dd9
7 changed files with 1676 additions and 140 deletions
4
.replit
4
.replit
|
|
@ -21,6 +21,10 @@ externalPort = 80
|
|||
localPort = 8080
|
||||
externalPort = 8080
|
||||
|
||||
[[ports]]
|
||||
localPort = 45655
|
||||
externalPort = 3000
|
||||
|
||||
[workflows]
|
||||
runButton = "Project"
|
||||
|
||||
|
|
|
|||
|
|
@ -718,7 +718,7 @@ if (fs.existsSync(sentinelPath)) {
|
|||
// =============================================================================
|
||||
|
||||
const listenersPath = path.join(__dirname, "listeners");
|
||||
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js', 'voiceXp.js', 'starboard.js'];
|
||||
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js', 'voiceXp.js', 'starboard.js', 'federationProtection.js'];
|
||||
for (const file of generalListenerFiles) {
|
||||
const filePath = path.join(listenersPath, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
|
|
@ -2500,33 +2500,22 @@ client.login(token).catch((error) => {
|
|||
});
|
||||
|
||||
// =============================================================================
|
||||
// DYNAMIC STATUS ROTATION
|
||||
// BOT STATUS - WATCHING PROTECTING THE FEDERATION
|
||||
// =============================================================================
|
||||
|
||||
function startDynamicStatus(client) {
|
||||
const statuses = [
|
||||
() => ({ name: `${client.guilds.cache.size} servers`, type: 3 }), // Watching
|
||||
() => ({ name: `${client.guilds.cache.reduce((sum, g) => sum + g.memberCount, 0).toLocaleString()} members`, type: 3 }), // Watching
|
||||
() => ({ name: '/help | aethex.studio', type: 0 }), // Playing
|
||||
() => ({ name: '🛡️ Guarding the Federation', type: 4 }), // Custom
|
||||
() => ({ name: 'for threats', type: 3 }), // Watching
|
||||
() => ({ name: '⚔️ Warden • Free Forever', type: 4 }), // Custom
|
||||
];
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
const updateStatus = () => {
|
||||
try {
|
||||
const status = statuses[currentIndex]();
|
||||
client.user.setActivity(status.name, { type: status.type });
|
||||
currentIndex = (currentIndex + 1) % statuses.length;
|
||||
} catch (e) {
|
||||
console.error('[Status] Error updating status:', e.message);
|
||||
}
|
||||
};
|
||||
|
||||
updateStatus();
|
||||
setInterval(updateStatus, 30000); // Rotate every 30 seconds
|
||||
function setWardenStatus(client) {
|
||||
try {
|
||||
client.user.setPresence({
|
||||
activities: [{
|
||||
name: 'Protecting the Federation',
|
||||
type: 3 // ActivityType.Watching
|
||||
}],
|
||||
status: 'online'
|
||||
});
|
||||
console.log('[Status] Set to: WATCHING Protecting the Federation');
|
||||
} catch (e) {
|
||||
console.error('[Status] Error setting status:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
client.once("clientReady", async () => {
|
||||
|
|
@ -2550,8 +2539,8 @@ client.once("clientReady", async () => {
|
|||
console.error("Failed to register commands:", regResult.error);
|
||||
}
|
||||
|
||||
// Dynamic rotating status
|
||||
startDynamicStatus(client);
|
||||
// Static status: WATCHING Protecting the Federation
|
||||
setWardenStatus(client);
|
||||
|
||||
if (setupFeedListener && supabase) {
|
||||
setupFeedListener(client);
|
||||
|
|
|
|||
|
|
@ -1,132 +1,696 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
|
||||
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
||||
const { getServerMode, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('federation')
|
||||
.setDescription('Manage cross-server role synchronization')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('link')
|
||||
.setDescription('Link a role for cross-server sync')
|
||||
.addRoleOption(option =>
|
||||
option.setName('role')
|
||||
.setDescription('Role to sync across servers')
|
||||
.setRequired(true)
|
||||
)
|
||||
.setDescription('AeThex Federation - Global protection & cross-server network')
|
||||
.addSubcommandGroup(group =>
|
||||
group
|
||||
.setName('roles')
|
||||
.setDescription('Manage cross-server role synchronization')
|
||||
.addSubcommand(sub => sub.setName('link').setDescription('Link a role for cross-server sync')
|
||||
.addRoleOption(opt => opt.setName('role').setDescription('Role to sync').setRequired(true)))
|
||||
.addSubcommand(sub => sub.setName('unlink').setDescription('Remove a role from sync')
|
||||
.addRoleOption(opt => opt.setName('role').setDescription('Role to remove').setRequired(true)))
|
||||
.addSubcommand(sub => sub.setName('list').setDescription('List all linked roles'))
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('unlink')
|
||||
.setDescription('Remove a role from sync')
|
||||
.addRoleOption(option =>
|
||||
option.setName('role')
|
||||
.setDescription('Role to remove from sync')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addSubcommandGroup(group =>
|
||||
group
|
||||
.setName('bans')
|
||||
.setDescription('Global Ban List - Protect the network')
|
||||
.addSubcommand(sub => sub.setName('add').setDescription('Add user to global ban list')
|
||||
.addUserOption(opt => opt.setName('user').setDescription('User to ban').setRequired(true))
|
||||
.addStringOption(opt => opt.setName('reason').setDescription('Reason for ban').setRequired(true))
|
||||
.addStringOption(opt => opt.setName('severity').setDescription('Severity level')
|
||||
.addChoices(
|
||||
{ name: 'Low - Minor offense', value: 'low' },
|
||||
{ name: 'Medium - Moderate offense', value: 'medium' },
|
||||
{ name: 'High - Serious offense', value: 'high' },
|
||||
{ name: 'Critical - Nuker/Scammer', value: 'critical' }
|
||||
)))
|
||||
.addSubcommand(sub => sub.setName('lookup').setDescription('Check if user is on global ban list')
|
||||
.addUserOption(opt => opt.setName('user').setDescription('User to lookup').setRequired(true)))
|
||||
.addSubcommand(sub => sub.setName('list').setDescription('View recent global bans'))
|
||||
.addSubcommand(sub => sub.setName('remove').setDescription('Remove user from global ban list')
|
||||
.addUserOption(opt => opt.setName('user').setDescription('User to remove').setRequired(true)))
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('List all linked roles')
|
||||
.addSubcommandGroup(group =>
|
||||
group
|
||||
.setName('servers')
|
||||
.setDescription('Federation Server Directory')
|
||||
.addSubcommand(sub => sub.setName('directory').setDescription('Browse all federation servers'))
|
||||
.addSubcommand(sub => sub.setName('info').setDescription('View a specific server')
|
||||
.addStringOption(opt => opt.setName('server').setDescription('Server name or ID').setRequired(true)))
|
||||
.addSubcommand(sub => sub.setName('featured').setDescription('View featured servers'))
|
||||
)
|
||||
.addSubcommandGroup(group =>
|
||||
group
|
||||
.setName('membership')
|
||||
.setDescription('Join or manage federation membership')
|
||||
.addSubcommand(sub => sub.setName('apply').setDescription('Apply to join the federation')
|
||||
.addStringOption(opt => opt.setName('category').setDescription('Server category')
|
||||
.addChoices(
|
||||
{ name: 'Gaming', value: 'gaming' },
|
||||
{ name: 'Creative', value: 'creative' },
|
||||
{ name: 'Development', value: 'development' },
|
||||
{ name: 'Education', value: 'education' },
|
||||
{ name: 'Community', value: 'community' },
|
||||
{ name: 'Business', value: 'business' }
|
||||
).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('treaty').setDescription('View the Federation Treaty'))
|
||||
)
|
||||
.addSubcommandGroup(group =>
|
||||
group
|
||||
.setName('scouts')
|
||||
.setDescription('Talent Scout - Cross-server reputation')
|
||||
.addSubcommand(sub => sub.setName('leaderboard').setDescription('View global reputation leaderboard'))
|
||||
.addSubcommand(sub => sub.setName('profile').setDescription('View cross-server profile')
|
||||
.addUserOption(opt => opt.setName('user').setDescription('User to lookup')))
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
const group = interaction.options.getSubcommandGroup();
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
if (mode === 'standalone') {
|
||||
if (mode === 'standalone' && group !== 'membership') {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.standalone)
|
||||
.setTitle('🏠 Standalone Mode')
|
||||
.setDescription('Federation features are disabled in standalone mode.\n\nThis server operates independently and does not sync roles across the AeThex network.\n\nUse `/config mode` to switch to federated mode.');
|
||||
.setTitle('Standalone Mode')
|
||||
.setDescription('Federation features are disabled in standalone mode.\n\nUse `/federation membership apply` to join the network, or `/config mode` to switch to federated mode.');
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
if (subcommand === 'link') {
|
||||
const role = interaction.options.getRole('role');
|
||||
|
||||
const mappingData = {
|
||||
name: role.name,
|
||||
guildId: interaction.guildId,
|
||||
guildName: interaction.guild.name,
|
||||
linkedAt: Date.now(),
|
||||
};
|
||||
client.federationMappings.set(role.id, mappingData);
|
||||
|
||||
if (client.saveFederationMapping) {
|
||||
await client.saveFederationMapping(role.id, mappingData);
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('Role Linked')
|
||||
.setDescription(`${role} is now linked for federation sync.`)
|
||||
.addFields(
|
||||
{ name: 'Role ID', value: role.id, inline: true },
|
||||
{ name: 'Server', value: interaction.guild.name, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
if (subcommand === 'unlink') {
|
||||
const role = interaction.options.getRole('role');
|
||||
|
||||
if (client.federationMappings.has(role.id)) {
|
||||
client.federationMappings.delete(role.id);
|
||||
|
||||
if (client.deleteFederationMapping) {
|
||||
await client.deleteFederationMapping(role.id);
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xff6600)
|
||||
.setTitle('Role Unlinked')
|
||||
.setDescription(`${role} has been removed from federation sync.`)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
} else {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setTitle('Not Found')
|
||||
.setDescription(`${role} is not currently linked.`)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
const mappings = [...client.federationMappings.entries()];
|
||||
|
||||
if (mappings.length === 0) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('Federation Roles')
|
||||
.setDescription('No roles are currently linked for federation sync.\nUse `/federation link` to add roles.')
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
return;
|
||||
}
|
||||
|
||||
const roleList = mappings.map(([roleId, data]) =>
|
||||
`<@&${roleId}> - ${data.guildName}`
|
||||
).join('\n');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('Federation Roles')
|
||||
.setDescription(roleList)
|
||||
.setFooter({ text: `${mappings.length} role(s) linked` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
if (group === 'roles') {
|
||||
await handleRoles(interaction, supabase, client, subcommand);
|
||||
} else if (group === 'bans') {
|
||||
await handleBans(interaction, supabase, client, subcommand);
|
||||
} else if (group === 'servers') {
|
||||
await handleServers(interaction, supabase, client, subcommand);
|
||||
} else if (group === 'membership') {
|
||||
await handleMembership(interaction, supabase, client, subcommand);
|
||||
} else if (group === 'scouts') {
|
||||
await handleScouts(interaction, supabase, client, subcommand);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
async function handleRoles(interaction, supabase, client, subcommand) {
|
||||
if (subcommand === 'link') {
|
||||
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageRoles)) {
|
||||
return interaction.reply({ content: 'You need Manage Roles permission.', ephemeral: true });
|
||||
}
|
||||
|
||||
const role = interaction.options.getRole('role');
|
||||
const mappingData = {
|
||||
name: role.name,
|
||||
guildId: interaction.guildId,
|
||||
guildName: interaction.guild.name,
|
||||
linkedAt: Date.now(),
|
||||
};
|
||||
client.federationMappings.set(role.id, mappingData);
|
||||
|
||||
if (client.saveFederationMapping) {
|
||||
await client.saveFederationMapping(role.id, mappingData);
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('Role Linked')
|
||||
.setDescription(`${role} is now linked for federation sync.`)
|
||||
.addFields(
|
||||
{ name: 'Role ID', value: role.id, inline: true },
|
||||
{ name: 'Server', value: interaction.guild.name, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
if (subcommand === 'unlink') {
|
||||
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageRoles)) {
|
||||
return interaction.reply({ content: 'You need Manage Roles permission.', ephemeral: true });
|
||||
}
|
||||
|
||||
const role = interaction.options.getRole('role');
|
||||
|
||||
if (client.federationMappings.has(role.id)) {
|
||||
client.federationMappings.delete(role.id);
|
||||
if (client.deleteFederationMapping) {
|
||||
await client.deleteFederationMapping(role.id);
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xff6600)
|
||||
.setTitle('Role Unlinked')
|
||||
.setDescription(`${role} has been removed from federation sync.`)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
} else {
|
||||
await interaction.reply({ content: `${role} is not currently linked.`, ephemeral: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
const mappings = [...client.federationMappings.entries()];
|
||||
|
||||
if (mappings.length === 0) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('Federation Roles')
|
||||
.setDescription('No roles are currently linked.\nUse `/federation roles link` to add roles.')
|
||||
.setTimestamp();
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const roleList = mappings.map(([roleId, data]) =>
|
||||
`<@&${roleId}> - ${data.guildName}`
|
||||
).join('\n');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('Federation Roles')
|
||||
.setDescription(roleList)
|
||||
.setFooter({ text: `${mappings.length} role(s) linked` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBans(interaction, supabase, client, subcommand) {
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Database not available.', ephemeral: true });
|
||||
}
|
||||
|
||||
if (subcommand === 'add') {
|
||||
if (!interaction.member.permissions.has(PermissionFlagsBits.BanMembers)) {
|
||||
return interaction.reply({ content: 'You need Ban Members permission.', ephemeral: true });
|
||||
}
|
||||
|
||||
const user = interaction.options.getUser('user');
|
||||
const reason = interaction.options.getString('reason');
|
||||
const severity = interaction.options.getString('severity') || 'medium';
|
||||
|
||||
const { data: existing } = await supabase
|
||||
.from('federation_bans')
|
||||
.select('id')
|
||||
.eq('user_id', user.id)
|
||||
.eq('active', true)
|
||||
.maybeSingle();
|
||||
|
||||
if (existing) {
|
||||
return interaction.reply({ content: `${user.tag} is already on the global ban list.`, ephemeral: true });
|
||||
}
|
||||
|
||||
const { error } = await supabase.from('federation_bans').insert({
|
||||
user_id: user.id,
|
||||
username: user.tag,
|
||||
reason,
|
||||
severity,
|
||||
banned_by_guild_id: interaction.guildId,
|
||||
banned_by_user_id: interaction.user.id,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Federation ban error:', error);
|
||||
return interaction.reply({ content: 'Failed to add ban.', ephemeral: true });
|
||||
}
|
||||
|
||||
const severityColors = { low: 0xffff00, medium: 0xff9900, high: 0xff3300, critical: 0xff0000 };
|
||||
const severityEmojis = { low: '⚠️', medium: '🔶', high: '🔴', critical: '☠️' };
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(severityColors[severity])
|
||||
.setTitle(`${severityEmojis[severity]} Global Ban Added`)
|
||||
.setThumbnail(user.displayAvatarURL())
|
||||
.addFields(
|
||||
{ name: 'User', value: `${user.tag}\n\`${user.id}\``, inline: true },
|
||||
{ name: 'Severity', value: severity.toUpperCase(), inline: true },
|
||||
{ name: 'Reason', value: reason }
|
||||
)
|
||||
.setFooter({ text: `Added by ${interaction.user.tag}` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
|
||||
await createBanAlerts(supabase, client, user.id, severity);
|
||||
}
|
||||
|
||||
if (subcommand === 'lookup') {
|
||||
const user = interaction.options.getUser('user');
|
||||
|
||||
const { data: ban } = await supabase
|
||||
.from('federation_bans')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.eq('active', true)
|
||||
.maybeSingle();
|
||||
|
||||
if (!ban) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('User Clear')
|
||||
.setThumbnail(user.displayAvatarURL())
|
||||
.setDescription(`${user.tag} is **not** on the global ban list.`)
|
||||
.setTimestamp();
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const severityColors = { low: 0xffff00, medium: 0xff9900, high: 0xff3300, critical: 0xff0000 };
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(severityColors[ban.severity])
|
||||
.setTitle('User Flagged')
|
||||
.setThumbnail(user.displayAvatarURL())
|
||||
.addFields(
|
||||
{ name: 'User', value: `${user.tag}\n\`${user.id}\``, inline: true },
|
||||
{ name: 'Severity', value: ban.severity.toUpperCase(), inline: true },
|
||||
{ name: 'Reason', value: ban.reason },
|
||||
{ name: 'Banned On', value: new Date(ban.created_at).toLocaleDateString(), inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
const { data: bans } = await supabase
|
||||
.from('federation_bans')
|
||||
.select('*')
|
||||
.eq('active', true)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
if (!bans || bans.length === 0) {
|
||||
return interaction.reply({ content: 'No active global bans.', ephemeral: true });
|
||||
}
|
||||
|
||||
const severityEmojis = { low: '⚠️', medium: '🔶', high: '🔴', critical: '☠️' };
|
||||
|
||||
const banList = bans.map(b =>
|
||||
`${severityEmojis[b.severity]} **${b.username || b.user_id}** - ${b.reason.substring(0, 50)}${b.reason.length > 50 ? '...' : ''}`
|
||||
).join('\n');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setTitle('Global Ban List')
|
||||
.setDescription(banList)
|
||||
.setFooter({ text: `Showing ${bans.length} most recent bans` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
if (subcommand === 'remove') {
|
||||
if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
|
||||
return interaction.reply({ content: 'You need Administrator permission.', ephemeral: true });
|
||||
}
|
||||
|
||||
const user = interaction.options.getUser('user');
|
||||
|
||||
const { error } = await supabase
|
||||
.from('federation_bans')
|
||||
.update({ active: false, updated_at: new Date().toISOString() })
|
||||
.eq('user_id', user.id)
|
||||
.eq('active', true);
|
||||
|
||||
if (error) {
|
||||
return interaction.reply({ content: 'Failed to remove ban.', ephemeral: true });
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('Ban Removed')
|
||||
.setDescription(`${user.tag} has been removed from the global ban list.`)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleServers(interaction, supabase, client, subcommand) {
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Database not available.', ephemeral: true });
|
||||
}
|
||||
|
||||
if (subcommand === 'directory') {
|
||||
const { data: servers } = await supabase
|
||||
.from('federation_servers')
|
||||
.select('*')
|
||||
.eq('status', 'approved')
|
||||
.order('member_count', { ascending: false })
|
||||
.limit(15);
|
||||
|
||||
if (!servers || servers.length === 0) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('Federation Directory')
|
||||
.setDescription('No servers in the federation yet.\nUse `/federation membership apply` to be the first!')
|
||||
.setTimestamp();
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const categoryEmojis = { gaming: '🎮', creative: '🎨', development: '💻', education: '📚', community: '👥', business: '🏢' };
|
||||
|
||||
const serverList = servers.map((s, i) => {
|
||||
const emoji = categoryEmojis[s.category] || '🌐';
|
||||
const featured = s.featured ? '⭐' : '';
|
||||
return `${i + 1}. ${emoji} **${s.guild_name}** ${featured}\n ${s.member_count?.toLocaleString() || '?'} members • ${s.category || 'general'}`;
|
||||
}).join('\n\n');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('Federation Directory')
|
||||
.setDescription(serverList)
|
||||
.setFooter({ text: `${servers.length} servers in the federation` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
if (subcommand === 'info') {
|
||||
const serverQuery = interaction.options.getString('server');
|
||||
|
||||
const { data: server } = await supabase
|
||||
.from('federation_servers')
|
||||
.select('*')
|
||||
.or(`guild_id.eq.${serverQuery},guild_name.ilike.%${serverQuery}%`)
|
||||
.eq('status', 'approved')
|
||||
.maybeSingle();
|
||||
|
||||
if (!server) {
|
||||
return interaction.reply({ content: 'Server not found in the federation.', ephemeral: true });
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle(server.guild_name)
|
||||
.setDescription(server.description || 'No description provided.')
|
||||
.addFields(
|
||||
{ name: 'Members', value: server.member_count?.toLocaleString() || 'Unknown', inline: true },
|
||||
{ name: 'Category', value: server.category || 'General', inline: true },
|
||||
{ name: 'Tier', value: server.tier?.toUpperCase() || 'FREE', inline: true },
|
||||
{ name: 'Joined Federation', value: new Date(server.joined_at).toLocaleDateString(), inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
if (server.guild_icon) {
|
||||
embed.setThumbnail(`https://cdn.discordapp.com/icons/${server.guild_id}/${server.guild_icon}.png`);
|
||||
}
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
if (subcommand === 'featured') {
|
||||
const { data: featured } = await supabase
|
||||
.from('federation_servers')
|
||||
.select('*')
|
||||
.eq('featured', true)
|
||||
.eq('status', 'approved');
|
||||
|
||||
if (!featured || featured.length === 0) {
|
||||
return interaction.reply({ content: 'No featured servers at this time.', ephemeral: true });
|
||||
}
|
||||
|
||||
const serverList = featured.map(s =>
|
||||
`⭐ **${s.guild_name}**\n${s.description?.substring(0, 100) || 'No description'}${s.description?.length > 100 ? '...' : ''}`
|
||||
).join('\n\n');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xffd700)
|
||||
.setTitle('Featured Servers')
|
||||
.setDescription(serverList)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMembership(interaction, supabase, client, subcommand) {
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Database not available.', ephemeral: true });
|
||||
}
|
||||
|
||||
if (subcommand === 'apply') {
|
||||
if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
|
||||
return interaction.reply({ content: 'Only administrators can apply to the federation.', ephemeral: true });
|
||||
}
|
||||
|
||||
const { data: existing } = await supabase
|
||||
.from('federation_servers')
|
||||
.select('status')
|
||||
.eq('guild_id', interaction.guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (existing) {
|
||||
return interaction.reply({ content: `Your server is already ${existing.status} in the federation.`, ephemeral: true });
|
||||
}
|
||||
|
||||
const { data: pendingApp } = await supabase
|
||||
.from('federation_applications')
|
||||
.select('status')
|
||||
.eq('guild_id', interaction.guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (pendingApp) {
|
||||
return interaction.reply({ content: `Application already ${pendingApp.status}. Use /federation membership status to check.`, ephemeral: true });
|
||||
}
|
||||
|
||||
const category = interaction.options.getString('category');
|
||||
const description = interaction.options.getString('description');
|
||||
|
||||
const { error } = await supabase.from('federation_applications').insert({
|
||||
guild_id: interaction.guildId,
|
||||
guild_name: interaction.guild.name,
|
||||
guild_icon: interaction.guild.icon,
|
||||
member_count: interaction.guild.memberCount,
|
||||
category,
|
||||
description,
|
||||
admin_id: interaction.user.id,
|
||||
admin_username: interaction.user.tag,
|
||||
treaty_agreed: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Federation application error:', error);
|
||||
return interaction.reply({ content: 'Failed to submit application.', ephemeral: true });
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('Application Submitted')
|
||||
.setDescription('Your server has been submitted for federation membership!')
|
||||
.addFields(
|
||||
{ name: 'Server', value: interaction.guild.name, inline: true },
|
||||
{ name: 'Category', value: category, inline: true },
|
||||
{ name: 'Status', value: 'Pending Review', inline: true }
|
||||
)
|
||||
.setFooter({ text: 'You will be notified when your application is reviewed.' })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
if (subcommand === 'status') {
|
||||
const { data: server } = await supabase
|
||||
.from('federation_servers')
|
||||
.select('*')
|
||||
.eq('guild_id', interaction.guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (server) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('Federation Member')
|
||||
.addFields(
|
||||
{ name: 'Status', value: server.status.toUpperCase(), inline: true },
|
||||
{ name: 'Tier', value: server.tier?.toUpperCase() || 'FREE', inline: true },
|
||||
{ name: 'Joined', value: new Date(server.joined_at).toLocaleDateString(), inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const { data: application } = await supabase
|
||||
.from('federation_applications')
|
||||
.select('*')
|
||||
.eq('guild_id', interaction.guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (application) {
|
||||
const statusColors = { pending: 0xffff00, approved: 0x00ff00, rejected: 0xff0000 };
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(statusColors[application.status] || 0x7c3aed)
|
||||
.setTitle('Application Status')
|
||||
.addFields(
|
||||
{ name: 'Status', value: application.status.toUpperCase(), inline: true },
|
||||
{ name: 'Submitted', value: new Date(application.created_at).toLocaleDateString(), inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
if (application.rejection_reason) {
|
||||
embed.addFields({ name: 'Reason', value: application.rejection_reason });
|
||||
}
|
||||
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('Not a Member')
|
||||
.setDescription('This server is not in the federation.\nUse `/federation membership apply` to join!')
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
if (subcommand === 'treaty') {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('The AeThex Federation Treaty')
|
||||
.setDescription('By joining the Federation, your server agrees to:')
|
||||
.addFields(
|
||||
{ name: '1. Contribute to Global Safety', value: 'Report nukers, scammers, and bad actors to the Global Ban List.' },
|
||||
{ name: '2. Maintain Standards', value: 'Uphold basic moderation and community guidelines.' },
|
||||
{ name: '3. Respect the Network', value: 'Do not exploit federation features or share protected data.' },
|
||||
{ name: '4. Display Membership', value: 'Optionally display Federation badge to verify authenticity.' }
|
||||
)
|
||||
.setFooter({ text: 'Together, we are untouchable.' })
|
||||
.setTimestamp();
|
||||
|
||||
const row = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('federation_agree_treaty')
|
||||
.setLabel('I Agree')
|
||||
.setStyle(ButtonStyle.Success),
|
||||
new ButtonBuilder()
|
||||
.setLabel('Learn More')
|
||||
.setStyle(ButtonStyle.Link)
|
||||
.setURL('https://aethex.dev/federation')
|
||||
);
|
||||
|
||||
await interaction.reply({ embeds: [embed], components: [row] });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleScouts(interaction, supabase, client, subcommand) {
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Database not available.', ephemeral: true });
|
||||
}
|
||||
|
||||
if (subcommand === 'leaderboard') {
|
||||
const { data: leaders } = await supabase
|
||||
.from('federation_reputation')
|
||||
.select('*')
|
||||
.order('reputation_score', { ascending: false })
|
||||
.limit(15);
|
||||
|
||||
if (!leaders || leaders.length === 0) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('Federation Leaderboard')
|
||||
.setDescription('No reputation data yet. Be active across federation servers to appear here!')
|
||||
.setTimestamp();
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const tierEmojis = { newcomer: '🌱', member: '⭐', veteran: '🏆', elite: '💎', legend: '👑' };
|
||||
|
||||
const leaderList = leaders.map((l, i) => {
|
||||
const emoji = tierEmojis[l.rank_tier] || '🌱';
|
||||
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;
|
||||
return `${medal} ${emoji} <@${l.discord_id}> - **${l.reputation_score?.toLocaleString() || 0}** rep`;
|
||||
}).join('\n');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xffd700)
|
||||
.setTitle('Federation Leaderboard')
|
||||
.setDescription(leaderList)
|
||||
.setFooter({ text: 'Reputation earned across all federation servers' })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
if (subcommand === 'profile') {
|
||||
const user = interaction.options.getUser('user') || interaction.user;
|
||||
|
||||
const { data: rep } = await supabase
|
||||
.from('federation_reputation')
|
||||
.select('*')
|
||||
.eq('discord_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (!rep) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('No Federation Profile')
|
||||
.setDescription(`${user.tag} doesn't have a federation profile yet.\nBe active across federation servers to build reputation!`)
|
||||
.setTimestamp();
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const tierEmojis = { newcomer: '🌱', member: '⭐', veteran: '🏆', elite: '💎', legend: '👑' };
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle(`${user.tag}'s Federation Profile`)
|
||||
.setThumbnail(user.displayAvatarURL())
|
||||
.addFields(
|
||||
{ name: 'Reputation', value: rep.reputation_score?.toLocaleString() || '0', inline: true },
|
||||
{ name: 'Rank', value: `${tierEmojis[rep.rank_tier] || '🌱'} ${rep.rank_tier?.toUpperCase() || 'NEWCOMER'}`, inline: true },
|
||||
{ name: 'Active In', value: `${rep.servers_active_in || 0} servers`, inline: true },
|
||||
{ name: 'Total XP', value: rep.total_xp?.toLocaleString() || '0', inline: true },
|
||||
{ name: 'Highest Level', value: `${rep.highest_level || 0}`, inline: true },
|
||||
{ name: 'Prestige', value: `${rep.prestige_total || 0}`, inline: true }
|
||||
)
|
||||
.setFooter({ text: `Last active: ${rep.last_active ? new Date(rep.last_active).toLocaleDateString() : 'Unknown'}` })
|
||||
.setTimestamp();
|
||||
|
||||
if (rep.badges && rep.badges.length > 0) {
|
||||
embed.addFields({ name: 'Badges', value: rep.badges.join(' ') });
|
||||
}
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
|
||||
async function createBanAlerts(supabase, client, userId, severity) {
|
||||
try {
|
||||
const { data: servers } = await supabase
|
||||
.from('federation_servers')
|
||||
.select('guild_id, tier')
|
||||
.eq('status', 'approved');
|
||||
|
||||
if (!servers) return;
|
||||
|
||||
const alerts = servers
|
||||
.filter(s => severity === 'critical' || s.tier === 'premium')
|
||||
.map(s => ({
|
||||
guild_id: s.guild_id,
|
||||
alert_type: 'new_ban',
|
||||
}));
|
||||
|
||||
if (alerts.length > 0) {
|
||||
const { data: ban } = await supabase
|
||||
.from('federation_bans')
|
||||
.select('id')
|
||||
.eq('user_id', userId)
|
||||
.eq('active', true)
|
||||
.maybeSingle();
|
||||
|
||||
if (ban) {
|
||||
const alertsWithBanId = alerts.map(a => ({ ...a, ban_id: ban.id }));
|
||||
await supabase.from('federation_alerts').insert(alertsWithBanId);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error creating ban alerts:', err);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
118
aethex-bot/listeners/federationProtection.js
Normal file
118
aethex-bot/listeners/federationProtection.js
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
const { EmbedBuilder } = require('discord.js');
|
||||
|
||||
module.exports = {
|
||||
name: 'guildMemberAdd',
|
||||
|
||||
async execute(member, client, supabase) {
|
||||
if (!supabase) return;
|
||||
|
||||
try {
|
||||
const { data: serverConfig } = await supabase
|
||||
.from('federation_servers')
|
||||
.select('tier, status')
|
||||
.eq('guild_id', member.guild.id)
|
||||
.eq('status', 'approved')
|
||||
.maybeSingle();
|
||||
|
||||
if (!serverConfig) return;
|
||||
|
||||
const { data: ban } = await supabase
|
||||
.from('federation_bans')
|
||||
.select('*')
|
||||
.eq('user_id', member.id)
|
||||
.eq('active', true)
|
||||
.maybeSingle();
|
||||
|
||||
if (!ban) return;
|
||||
|
||||
const isPremium = serverConfig.tier === 'premium';
|
||||
const isCritical = ban.severity === 'critical';
|
||||
|
||||
if (!isPremium && !isCritical) {
|
||||
return;
|
||||
}
|
||||
|
||||
const severityColors = { low: 0xffff00, medium: 0xff9900, high: 0xff3300, critical: 0xff0000 };
|
||||
const severityEmojis = { low: '⚠️', medium: '🔶', high: '🔴', critical: '☠️' };
|
||||
|
||||
const alertEmbed = new EmbedBuilder()
|
||||
.setColor(severityColors[ban.severity])
|
||||
.setTitle(`${severityEmojis[ban.severity]} Federation Alert: Flagged User Joined`)
|
||||
.setThumbnail(member.displayAvatarURL())
|
||||
.addFields(
|
||||
{ name: 'User', value: `${member.user.tag}\n\`${member.id}\``, inline: true },
|
||||
{ name: 'Severity', value: ban.severity.toUpperCase(), inline: true },
|
||||
{ name: 'Reason', value: ban.reason || 'No reason provided' }
|
||||
)
|
||||
.setFooter({ text: 'AeThex Federation Protection' })
|
||||
.setTimestamp();
|
||||
|
||||
const { data: config } = await supabase
|
||||
.from('server_config')
|
||||
.select('modlog_channel')
|
||||
.eq('guild_id', member.guild.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (config?.modlog_channel) {
|
||||
const logChannel = await client.channels.fetch(config.modlog_channel).catch(() => null);
|
||||
if (logChannel) {
|
||||
await logChannel.send({ embeds: [alertEmbed] });
|
||||
}
|
||||
}
|
||||
|
||||
if (isCritical) {
|
||||
try {
|
||||
await member.ban({ reason: `[Federation] Global ban: ${ban.reason}` });
|
||||
|
||||
alertEmbed.setTitle(`${severityEmojis[ban.severity]} Federation Auto-Ban: Critical Threat Removed`);
|
||||
alertEmbed.addFields({ name: 'Action Taken', value: 'User was automatically banned' });
|
||||
|
||||
await supabase.from('federation_alerts').update({
|
||||
delivered: true,
|
||||
delivered_at: new Date().toISOString(),
|
||||
action_taken: 'auto_ban'
|
||||
}).eq('guild_id', member.guild.id).eq('ban_id', ban.id);
|
||||
|
||||
} catch (banError) {
|
||||
console.error('[Federation] Failed to auto-ban:', banError.message);
|
||||
alertEmbed.addFields({ name: 'Action Required', value: 'Auto-ban failed. Please ban manually.' });
|
||||
}
|
||||
} else if (isPremium) {
|
||||
try {
|
||||
await member.kick(`[Federation] Global ban (${ban.severity} severity): ${ban.reason}`);
|
||||
alertEmbed.addFields({ name: 'Action Taken', value: 'User was automatically kicked (Premium Protection)' });
|
||||
|
||||
await supabase.from('federation_alerts').update({
|
||||
delivered: true,
|
||||
delivered_at: new Date().toISOString(),
|
||||
action_taken: 'auto_kick'
|
||||
}).eq('guild_id', member.guild.id).eq('ban_id', ban.id);
|
||||
} catch (kickError) {
|
||||
console.error('[Federation] Failed to auto-kick:', kickError.message);
|
||||
alertEmbed.addFields({ name: 'Action Required', value: 'Auto-kick failed. Please remove user manually.' });
|
||||
}
|
||||
}
|
||||
|
||||
const owner = await member.guild.fetchOwner().catch(() => null);
|
||||
if (owner && ban.severity === 'critical') {
|
||||
try {
|
||||
const dmEmbed = new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setTitle('Federation Alert: Critical Threat')
|
||||
.setDescription(`A user on the global ban list (Critical severity) joined **${member.guild.name}** and was automatically banned.`)
|
||||
.addFields(
|
||||
{ name: 'User', value: `${member.user.tag} (\`${member.id}\`)` },
|
||||
{ name: 'Reason', value: ban.reason }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await owner.send({ embeds: [dmEmbed] });
|
||||
} catch (dmError) {
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Federation Protection] Error:', error.message);
|
||||
}
|
||||
},
|
||||
};
|
||||
625
aethex-bot/public/federation.html
Normal file
625
aethex-bot/public/federation.html
Normal file
|
|
@ -0,0 +1,625 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Federation - AeThex | Warden</title>
|
||||
<style>
|
||||
:root {
|
||||
--background: #030712;
|
||||
--foreground: #f8fafc;
|
||||
--card: rgba(15, 23, 42, 0.6);
|
||||
--card-border: rgba(99, 102, 241, 0.15);
|
||||
--card-border-hover: rgba(99, 102, 241, 0.4);
|
||||
--primary: #6366f1;
|
||||
--primary-light: #818cf8;
|
||||
--secondary: rgba(30, 41, 59, 0.5);
|
||||
--muted: #64748b;
|
||||
--border: rgba(51, 65, 85, 0.5);
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
min-height: 100vh;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.bg-grid {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(99, 102, 241, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(99, 102, 241, 0.03) 1px, transparent 1px);
|
||||
background-size: 64px 64px;
|
||||
pointer-events: none;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
header {
|
||||
background: rgba(3, 7, 18, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 1rem 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.logo-icon { width: 40px; height: 40px; border-radius: 8px; }
|
||||
.logo-text { font-size: 1.25rem; font-weight: 700; }
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.nav-links a:hover, .nav-links a.active { color: var(--foreground); }
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary), #3b82f6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--secondary);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.btn-success { background: var(--success); color: white; }
|
||||
.btn-danger { background: var(--danger); color: white; }
|
||||
.btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8rem; }
|
||||
|
||||
.hero {
|
||||
padding: 3rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
color: var(--muted);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, var(--primary), #3b82f6, #06b6d4);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--primary), #3b82f6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: var(--muted);
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover { color: var(--foreground); }
|
||||
|
||||
.tab.active {
|
||||
background: var(--card);
|
||||
border-color: var(--primary);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.section { display: none; }
|
||||
.section.active { display: block; }
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-title { font-size: 1.1rem; font-weight: 600; }
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.table th {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-low { background: rgba(255, 255, 0, 0.2); color: #ffd700; }
|
||||
.badge-medium { background: rgba(255, 153, 0, 0.2); color: #ff9900; }
|
||||
.badge-high { background: rgba(255, 51, 0, 0.2); color: #ff3300; }
|
||||
.badge-critical { background: rgba(255, 0, 0, 0.2); color: #ff0000; }
|
||||
.badge-pending { background: rgba(255, 193, 7, 0.2); color: var(--warning); }
|
||||
.badge-approved { background: rgba(16, 185, 129, 0.2); color: var(--success); }
|
||||
|
||||
.server-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.server-card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.server-card:hover {
|
||||
border-color: var(--card-border-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.server-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: var(--secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.server-name { font-weight: 600; }
|
||||
.server-category { font-size: 0.8rem; color: var(--muted); }
|
||||
.server-desc { font-size: 0.9rem; color: var(--muted); margin-bottom: 0.75rem; }
|
||||
.server-members { font-size: 0.85rem; color: var(--primary-light); }
|
||||
|
||||
.leaderboard-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.leaderboard-rank {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
.leaderboard-rank.gold { background: linear-gradient(135deg, #ffd700, #ff8c00); color: #000; }
|
||||
.leaderboard-rank.silver { background: linear-gradient(135deg, #c0c0c0, #a0a0a0); color: #000; }
|
||||
.leaderboard-rank.bronze { background: linear-gradient(135deg, #cd7f32, #8b4513); color: #fff; }
|
||||
|
||||
.leaderboard-info { flex: 1; }
|
||||
.leaderboard-name { font-weight: 600; }
|
||||
.leaderboard-tier { font-size: 0.8rem; color: var(--muted); }
|
||||
.leaderboard-score { font-weight: 700; color: var(--primary-light); }
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid { grid-template-columns: 1fr; }
|
||||
.nav-links { display: none; }
|
||||
.tabs { overflow-x: auto; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-grid"></div>
|
||||
|
||||
<header>
|
||||
<div class="container header-content">
|
||||
<a href="/" class="logo">
|
||||
<img src="/logo.png" alt="AeThex" class="logo-icon">
|
||||
<span class="logo-text">AeThex | Warden</span>
|
||||
</a>
|
||||
|
||||
<nav class="nav-links">
|
||||
<a href="/">Home</a>
|
||||
<a href="/features">Features</a>
|
||||
<a href="/commands">Commands</a>
|
||||
<a href="/federation" class="active">Federation</a>
|
||||
<a href="/dashboard">Dashboard</a>
|
||||
</nav>
|
||||
|
||||
<a href="https://discord.com/api/oauth2/authorize?client_id=578971245454950421&permissions=8&scope=bot%20applications.commands" class="btn btn-primary" target="_blank">Add to Server</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1>The <span class="text-gradient">Federation</span></h1>
|
||||
<p>A network of protected servers. Ban one, ban all.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="totalServers">-</div>
|
||||
<div class="stat-label">Member Servers</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="activeBans">-</div>
|
||||
<div class="stat-label">Active Bans</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="pendingApps">-</div>
|
||||
<div class="stat-label">Pending Applications</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="servers">Servers</button>
|
||||
<button class="tab" data-tab="bans">Global Bans</button>
|
||||
<button class="tab" data-tab="applications">Applications</button>
|
||||
<button class="tab" data-tab="leaderboard">Leaderboard</button>
|
||||
</div>
|
||||
|
||||
<div id="servers" class="section active">
|
||||
<div class="server-grid" id="serverGrid">
|
||||
<div class="empty-state">Loading servers...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bans" class="section">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Global Ban List</span>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Severity</th>
|
||||
<th>Reason</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="banList">
|
||||
<tr><td colspan="4" class="empty-state">Loading bans...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="applications" class="section">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Pending Applications</span>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Server</th>
|
||||
<th>Category</th>
|
||||
<th>Members</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="appList">
|
||||
<tr><td colspan="5" class="empty-state">Loading applications...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="leaderboard" class="section">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Federation Reputation Leaders</span>
|
||||
</div>
|
||||
<div id="leaderboardList">
|
||||
<div class="empty-state">Loading leaderboard...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
document.getElementById(tab.dataset.tab).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await fetch('/api/federation/stats');
|
||||
const data = await res.json();
|
||||
document.getElementById('totalServers').textContent = data.totalServers || 0;
|
||||
document.getElementById('activeBans').textContent = data.activeBans || 0;
|
||||
document.getElementById('pendingApps').textContent = data.pendingApplications || 0;
|
||||
} catch (e) {
|
||||
console.error('Failed to load stats:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadServers() {
|
||||
try {
|
||||
const res = await fetch('/api/federation/servers');
|
||||
const data = await res.json();
|
||||
const grid = document.getElementById('serverGrid');
|
||||
|
||||
if (!data.servers || data.servers.length === 0) {
|
||||
grid.innerHTML = '<div class="empty-state">No servers in the federation yet. Use /federation membership apply to join!</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryEmojis = { gaming: '🎮', creative: '🎨', development: '💻', education: '📚', community: '👥', business: '🏢' };
|
||||
|
||||
grid.innerHTML = data.servers.map(s => `
|
||||
<div class="server-card">
|
||||
<div class="server-header">
|
||||
<div class="server-icon">${categoryEmojis[s.category] || '🌐'}</div>
|
||||
<div>
|
||||
<div class="server-name">${s.guild_name}</div>
|
||||
<div class="server-category">${s.category || 'General'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-desc">${s.description || 'No description'}</div>
|
||||
<div class="server-members">${(s.member_count || 0).toLocaleString()} members</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
console.error('Failed to load servers:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBans() {
|
||||
try {
|
||||
const res = await fetch('/api/federation/bans?limit=50');
|
||||
const data = await res.json();
|
||||
const tbody = document.getElementById('banList');
|
||||
|
||||
if (!data.bans || data.bans.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No active bans</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.bans.map(b => `
|
||||
<tr>
|
||||
<td>${b.username || b.user_id}</td>
|
||||
<td><span class="badge badge-${b.severity}">${b.severity.toUpperCase()}</span></td>
|
||||
<td>${b.reason.substring(0, 50)}${b.reason.length > 50 ? '...' : ''}</td>
|
||||
<td>${new Date(b.created_at).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
console.error('Failed to load bans:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadApplications() {
|
||||
try {
|
||||
const res = await fetch('/api/federation/applications');
|
||||
const data = await res.json();
|
||||
const tbody = document.getElementById('appList');
|
||||
|
||||
if (!data.applications || data.applications.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No applications</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.applications.map(a => `
|
||||
<tr>
|
||||
<td>${a.guild_name}</td>
|
||||
<td>${a.category || 'General'}</td>
|
||||
<td>${(a.member_count || 0).toLocaleString()}</td>
|
||||
<td><span class="badge badge-${a.status}">${a.status.toUpperCase()}</span></td>
|
||||
<td>
|
||||
${a.status === 'pending' ? `
|
||||
<button class="btn btn-success btn-sm" onclick="approveApp(${a.id})">Approve</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="rejectApp(${a.id})">Reject</button>
|
||||
` : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
console.error('Failed to load applications:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLeaderboard() {
|
||||
try {
|
||||
const res = await fetch('/api/federation/leaderboard?limit=20');
|
||||
const data = await res.json();
|
||||
const container = document.getElementById('leaderboardList');
|
||||
|
||||
if (!data.leaderboard || data.leaderboard.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">No reputation data yet. Be active across federation servers!</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const tierEmojis = { newcomer: '🌱', member: '⭐', veteran: '🏆', elite: '💎', legend: '👑' };
|
||||
|
||||
container.innerHTML = data.leaderboard.map((l, i) => {
|
||||
const rankClass = i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : '';
|
||||
return `
|
||||
<div class="leaderboard-item">
|
||||
<div class="leaderboard-rank ${rankClass}">${i + 1}</div>
|
||||
<div class="leaderboard-info">
|
||||
<div class="leaderboard-name">${l.discord_id}</div>
|
||||
<div class="leaderboard-tier">${tierEmojis[l.rank_tier] || '🌱'} ${(l.rank_tier || 'newcomer').toUpperCase()}</div>
|
||||
</div>
|
||||
<div class="leaderboard-score">${(l.reputation_score || 0).toLocaleString()} rep</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
console.error('Failed to load leaderboard:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function approveApp(id) {
|
||||
if (!confirm('Approve this application?')) return;
|
||||
try {
|
||||
await fetch(`/api/federation/applications/${id}/approve`, { method: 'POST' });
|
||||
loadApplications();
|
||||
loadStats();
|
||||
loadServers();
|
||||
} catch (e) {
|
||||
alert('Failed to approve');
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectApp(id) {
|
||||
const reason = prompt('Rejection reason:');
|
||||
if (!reason) return;
|
||||
try {
|
||||
await fetch(`/api/federation/applications/${id}/reject`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reason })
|
||||
});
|
||||
loadApplications();
|
||||
loadStats();
|
||||
} catch (e) {
|
||||
alert('Failed to reject');
|
||||
}
|
||||
}
|
||||
|
||||
loadStats();
|
||||
loadServers();
|
||||
loadBans();
|
||||
loadApplications();
|
||||
loadLeaderboard();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1038,6 +1038,192 @@ function createWebServer(discordClient, supabase, options = {}) {
|
|||
}
|
||||
});
|
||||
|
||||
// ============ FEDERATION API ============
|
||||
|
||||
app.get('/api/federation/stats', async (req, res) => {
|
||||
if (!supabase) {
|
||||
return res.status(503).json({ error: 'Database not available' });
|
||||
}
|
||||
|
||||
try {
|
||||
const [servers, bans, applications] = await Promise.all([
|
||||
supabase.from('federation_servers').select('id', { count: 'exact' }).eq('status', 'approved'),
|
||||
supabase.from('federation_bans').select('id', { count: 'exact' }).eq('active', true),
|
||||
supabase.from('federation_applications').select('id', { count: 'exact' }).eq('status', 'pending')
|
||||
]);
|
||||
|
||||
res.json({
|
||||
totalServers: servers.count || 0,
|
||||
activeBans: bans.count || 0,
|
||||
pendingApplications: applications.count || 0
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch stats' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/federation/bans', async (req, res) => {
|
||||
if (!supabase) {
|
||||
return res.status(503).json({ error: 'Database not available' });
|
||||
}
|
||||
|
||||
const { limit = 50, severity } = req.query;
|
||||
|
||||
try {
|
||||
let query = supabase
|
||||
.from('federation_bans')
|
||||
.select('*')
|
||||
.eq('active', true)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(parseInt(limit));
|
||||
|
||||
if (severity) {
|
||||
query = query.eq('severity', severity);
|
||||
}
|
||||
|
||||
const { data: bans } = await query;
|
||||
res.json({ bans: bans || [] });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch bans' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/federation/servers', async (req, res) => {
|
||||
if (!supabase) {
|
||||
return res.status(503).json({ error: 'Database not available' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: servers } = await supabase
|
||||
.from('federation_servers')
|
||||
.select('*')
|
||||
.eq('status', 'approved')
|
||||
.order('member_count', { ascending: false });
|
||||
|
||||
res.json({ servers: servers || [] });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch servers' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/federation/applications', 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' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: applications } = await supabase
|
||||
.from('federation_applications')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
res.json({ applications: applications || [] });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch applications' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/federation/applications/:appId/approve', 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 { appId } = req.params;
|
||||
|
||||
try {
|
||||
const { data: app } = await supabase
|
||||
.from('federation_applications')
|
||||
.select('*')
|
||||
.eq('id', appId)
|
||||
.maybeSingle();
|
||||
|
||||
if (!app) {
|
||||
return res.status(404).json({ error: 'Application not found' });
|
||||
}
|
||||
|
||||
await supabase.from('federation_applications').update({
|
||||
status: 'approved',
|
||||
reviewed_by: userId,
|
||||
reviewed_at: new Date().toISOString()
|
||||
}).eq('id', appId);
|
||||
|
||||
await supabase.from('federation_servers').insert({
|
||||
guild_id: app.guild_id,
|
||||
guild_name: app.guild_name,
|
||||
guild_icon: app.guild_icon,
|
||||
description: app.description,
|
||||
category: app.category,
|
||||
member_count: app.member_count,
|
||||
owner_id: app.admin_id,
|
||||
status: 'approved',
|
||||
treaty_accepted: true,
|
||||
treaty_accepted_at: new Date().toISOString()
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to approve application:', error);
|
||||
res.status(500).json({ error: 'Failed to approve application' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/federation/applications/:appId/reject', 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 { appId } = req.params;
|
||||
const { reason } = req.body;
|
||||
|
||||
try {
|
||||
await supabase.from('federation_applications').update({
|
||||
status: 'rejected',
|
||||
reviewed_by: userId,
|
||||
reviewed_at: new Date().toISOString(),
|
||||
rejection_reason: reason || 'No reason provided'
|
||||
}).eq('id', appId);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to reject application' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/federation/leaderboard', async (req, res) => {
|
||||
if (!supabase) {
|
||||
return res.status(503).json({ error: 'Database not available' });
|
||||
}
|
||||
|
||||
const { limit = 50 } = req.query;
|
||||
|
||||
try {
|
||||
const { data: leaders } = await supabase
|
||||
.from('federation_reputation')
|
||||
.select('*')
|
||||
.order('reputation_score', { ascending: false })
|
||||
.limit(parseInt(limit));
|
||||
|
||||
res.json({ leaderboard: leaders || [] });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch leaderboard' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
|
|
@ -1046,6 +1232,10 @@ function createWebServer(discordClient, supabase, options = {}) {
|
|||
});
|
||||
});
|
||||
|
||||
app.get('/federation', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../public/federation.html'));
|
||||
});
|
||||
|
||||
app.get('/dashboard', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../public/dashboard.html'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
This is a Network Effect strategy. You aren't selling the code; you are selling Membership.
|
||||
|
||||
If the AeThex | Warden bot allows external servers to "Join the Federation," you are essentially building a Digital Alliance (like NATO or the United Nations of Roblox).
|
||||
|
||||
Here is how you extract massive value (money, power, and data) from letting other servers join your Federation.
|
||||
|
||||
1. The "Global Ban List" (Data Supremacy)
|
||||
The Mechanism: When a Federation server bans a user for being a "Nuker" or "Scammer," that ID is pushed to your central Supabase database.
|
||||
|
||||
The Benefit: You build the ultimate Global Blacklist.
|
||||
|
||||
The Monetization:
|
||||
|
||||
Free Tier: Servers contribute bans but only get protection from "High Risk" nukers.
|
||||
|
||||
Paid Tier ($50/mo): Servers get real-time protection from all Federation bans. "Protect your server from scammers before they even join."
|
||||
|
||||
2. The "Cross-Pollination" (Traffic)
|
||||
The Mechanism: The bot includes a /federation directory or a "Featured Server" channel that is synced across all Federation members.
|
||||
|
||||
The Benefit: Free advertising for your projects (Lone Star).
|
||||
|
||||
The Monetization: You can charge member servers for "Featured Spots."
|
||||
|
||||
Example: "Want your game promoted to the 50,000 users across the entire Federation network? That's $200/week."
|
||||
|
||||
3. The "Diplomatic" Leverage (Power)
|
||||
The Mechanism: To join the Federation, a server admin must agree to your "Treaty" (Terms of Service).
|
||||
|
||||
The Benefit: You become the Standard Setter.
|
||||
|
||||
The Strategy: If a studio wants to be in the Federation (to look safe/official), they have to use your standards. This positions AeThex as the "Governing Body" of the alliance. It elevates your brand from "Studio" to "Authority."
|
||||
|
||||
4. The "Talent Scout" (Recruitment)
|
||||
The Mechanism: With Federation Sync, you can see high-level users across all connected servers.
|
||||
|
||||
The Benefit: You can identify top developers or active users in other people's servers.
|
||||
|
||||
The Strategy: If you see a user who is Level 50 in three different Federation coding servers, you know they are legit. You can invite them to StarFoundry directly.
|
||||
|
||||
Summary: How to Pitch It
|
||||
You don't sell the bot. You sell Safety in Numbers.
|
||||
|
||||
"Join the AeThex Federation. When a nuker attacks one of us, they are banned from all of us instantly. Together, we are untouchable."
|
||||
|
||||
Verdict: This is the smartest "Long Game" move. It costs you nothing but server fees, but it builds a massive defensive network that you control.
|
||||
Loading…
Reference in a new issue