Implement partnership management commands including request, list, pending, accept, reject, and end subcommands. Add a helper function to notify partner servers of partnership status changes. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: bf03030c-b05b-43d7-9556-dd29f9fb7953 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/INbSSam Replit-Helium-Checkpoint-Created: true
1035 lines
39 KiB
JavaScript
1035 lines
39 KiB
JavaScript
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('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'))
|
|
)
|
|
.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)))
|
|
)
|
|
.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')))
|
|
)
|
|
.addSubcommandGroup(group =>
|
|
group
|
|
.setName('partners')
|
|
.setDescription('Server partnership management')
|
|
.addSubcommand(sub => sub.setName('request').setDescription('Send a partnership request to another server')
|
|
.addStringOption(opt => opt.setName('server_id').setDescription('Server ID to partner with').setRequired(true))
|
|
.addStringOption(opt => opt.setName('message').setDescription('Partnership proposal message').setRequired(true).setMaxLength(500)))
|
|
.addSubcommand(sub => sub.setName('list').setDescription('View your server partnerships'))
|
|
.addSubcommand(sub => sub.setName('pending').setDescription('View pending partnership requests'))
|
|
.addSubcommand(sub => sub.setName('accept').setDescription('Accept a partnership request')
|
|
.addStringOption(opt => opt.setName('request_id').setDescription('Request ID to accept').setRequired(true)))
|
|
.addSubcommand(sub => sub.setName('reject').setDescription('Reject a partnership request')
|
|
.addStringOption(opt => opt.setName('request_id').setDescription('Request ID to reject').setRequired(true))
|
|
.addStringOption(opt => opt.setName('reason').setDescription('Rejection reason')))
|
|
.addSubcommand(sub => sub.setName('end').setDescription('End an existing partnership')
|
|
.addStringOption(opt => opt.setName('partner_id').setDescription('Partner server ID').setRequired(true)))
|
|
),
|
|
|
|
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' && group !== 'membership') {
|
|
const embed = new EmbedBuilder()
|
|
.setColor(EMBED_COLORS.standalone)
|
|
.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 });
|
|
}
|
|
|
|
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);
|
|
} else if (group === 'partners') {
|
|
await handlePartners(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);
|
|
}
|
|
}
|
|
|
|
async function notifyPartnerServer(client, supabase, targetGuildId, embed) {
|
|
try {
|
|
const targetGuild = client.guilds.cache.get(targetGuildId);
|
|
if (!targetGuild) return;
|
|
|
|
const { data: config } = await supabase
|
|
.from('server_config')
|
|
.select('mod_log_channel')
|
|
.eq('guild_id', targetGuildId)
|
|
.maybeSingle();
|
|
|
|
if (config?.mod_log_channel) {
|
|
const channel = targetGuild.channels.cache.get(config.mod_log_channel);
|
|
if (channel) {
|
|
await channel.send({ embeds: [embed] });
|
|
return;
|
|
}
|
|
}
|
|
|
|
const systemChannel = targetGuild.systemChannel;
|
|
if (systemChannel) {
|
|
await systemChannel.send({ embeds: [embed] });
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to notify partner server:', err);
|
|
}
|
|
}
|
|
|
|
async function handlePartners(interaction, supabase, client, subcommand) {
|
|
if (!supabase) {
|
|
return interaction.reply({ content: 'Database not available.', ephemeral: true });
|
|
}
|
|
|
|
if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
|
|
return interaction.reply({ content: 'Only administrators can manage partnerships.', ephemeral: true });
|
|
}
|
|
|
|
if (subcommand === 'request') {
|
|
const targetServerId = interaction.options.getString('server_id');
|
|
const message = interaction.options.getString('message');
|
|
|
|
if (targetServerId === interaction.guildId) {
|
|
return interaction.reply({ content: 'You cannot partner with your own server.', ephemeral: true });
|
|
}
|
|
|
|
const { data: targetServer } = await supabase
|
|
.from('federation_servers')
|
|
.select('guild_id, guild_name')
|
|
.eq('guild_id', targetServerId)
|
|
.eq('status', 'approved')
|
|
.maybeSingle();
|
|
|
|
if (!targetServer) {
|
|
return interaction.reply({ content: 'Target server is not in the federation or not found.', ephemeral: true });
|
|
}
|
|
|
|
const { data: existing } = await supabase
|
|
.from('server_partnerships')
|
|
.select('id, status')
|
|
.or(`and(requester_guild_id.eq.${interaction.guildId},target_guild_id.eq.${targetServerId}),and(requester_guild_id.eq.${targetServerId},target_guild_id.eq.${interaction.guildId})`)
|
|
.in('status', ['pending', 'accepted'])
|
|
.maybeSingle();
|
|
|
|
if (existing) {
|
|
return interaction.reply({
|
|
content: `A partnership ${existing.status === 'accepted' ? 'already exists' : 'request is pending'} with this server.`,
|
|
ephemeral: true
|
|
});
|
|
}
|
|
|
|
const { error } = await supabase.from('server_partnerships').insert({
|
|
requester_guild_id: interaction.guildId,
|
|
requester_guild_name: interaction.guild.name,
|
|
target_guild_id: targetServerId,
|
|
target_guild_name: targetServer.guild_name,
|
|
message,
|
|
status: 'pending',
|
|
});
|
|
|
|
if (error) {
|
|
console.error('Partnership request error:', error);
|
|
return interaction.reply({ content: 'Failed to send partnership request.', ephemeral: true });
|
|
}
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(0x00ff00)
|
|
.setTitle('Partnership Request Sent')
|
|
.setDescription(`Your partnership request has been sent to **${targetServer.guild_name}**!`)
|
|
.addFields(
|
|
{ name: 'Your Message', value: message },
|
|
{ name: 'Status', value: 'Pending', inline: true }
|
|
)
|
|
.setFooter({ text: 'They will be notified of your request.' })
|
|
.setTimestamp();
|
|
|
|
await interaction.reply({ embeds: [embed] });
|
|
}
|
|
|
|
if (subcommand === 'list') {
|
|
const { data: partnerships } = await supabase
|
|
.from('server_partnerships')
|
|
.select('*')
|
|
.eq('status', 'accepted')
|
|
.or(`requester_guild_id.eq.${interaction.guildId},target_guild_id.eq.${interaction.guildId}`)
|
|
.order('accepted_at', { ascending: false });
|
|
|
|
if (!partnerships || partnerships.length === 0) {
|
|
const embed = new EmbedBuilder()
|
|
.setColor(0x7c3aed)
|
|
.setTitle('Server Partnerships')
|
|
.setDescription('No active partnerships.\nUse `/federation partners request` to request one!')
|
|
.setTimestamp();
|
|
return interaction.reply({ embeds: [embed] });
|
|
}
|
|
|
|
const partnerList = partnerships.map((p, i) => {
|
|
const partnerName = p.requester_guild_id === interaction.guildId
|
|
? p.target_guild_name
|
|
: p.requester_guild_name;
|
|
const partnerId = p.requester_guild_id === interaction.guildId
|
|
? p.target_guild_id
|
|
: p.requester_guild_id;
|
|
const since = p.accepted_at ? new Date(p.accepted_at).toLocaleDateString() : 'Unknown';
|
|
return `${i + 1}. **${partnerName}** (ID: \`${partnerId}\`)\n Since: ${since}`;
|
|
}).join('\n\n');
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(0x7c3aed)
|
|
.setTitle('Server Partnerships')
|
|
.setDescription(partnerList)
|
|
.setFooter({ text: `${partnerships.length} active partnership(s)` })
|
|
.setTimestamp();
|
|
|
|
await interaction.reply({ embeds: [embed] });
|
|
}
|
|
|
|
if (subcommand === 'pending') {
|
|
const { data: pending } = await supabase
|
|
.from('server_partnerships')
|
|
.select('*')
|
|
.eq('target_guild_id', interaction.guildId)
|
|
.eq('status', 'pending')
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (!pending || pending.length === 0) {
|
|
const embed = new EmbedBuilder()
|
|
.setColor(0x7c3aed)
|
|
.setTitle('Pending Partnership Requests')
|
|
.setDescription('No pending partnership requests.')
|
|
.setTimestamp();
|
|
return interaction.reply({ embeds: [embed] });
|
|
}
|
|
|
|
const pendingList = pending.map(p => {
|
|
const date = new Date(p.created_at).toLocaleDateString();
|
|
return `**Request #${p.id}** from **${p.requester_guild_name}**\n` +
|
|
`> ${p.message?.substring(0, 100) || 'No message'}${p.message?.length > 100 ? '...' : ''}\n` +
|
|
`Received: ${date}`;
|
|
}).join('\n\n');
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(0xffff00)
|
|
.setTitle('Pending Partnership Requests')
|
|
.setDescription(pendingList)
|
|
.addFields({
|
|
name: 'Actions',
|
|
value: '`/federation partners accept <request_id>` - Accept a request\n' +
|
|
'`/federation partners reject <request_id>` - Reject a request'
|
|
})
|
|
.setFooter({ text: `${pending.length} pending request(s)` })
|
|
.setTimestamp();
|
|
|
|
await interaction.reply({ embeds: [embed] });
|
|
}
|
|
|
|
if (subcommand === 'accept') {
|
|
const requestId = interaction.options.getString('request_id');
|
|
|
|
const { data: request } = await supabase
|
|
.from('server_partnerships')
|
|
.select('*')
|
|
.eq('id', requestId)
|
|
.eq('target_guild_id', interaction.guildId)
|
|
.eq('status', 'pending')
|
|
.maybeSingle();
|
|
|
|
if (!request) {
|
|
return interaction.reply({ content: 'Partnership request not found or not pending.', ephemeral: true });
|
|
}
|
|
|
|
const { error } = await supabase
|
|
.from('server_partnerships')
|
|
.update({
|
|
status: 'accepted',
|
|
accepted_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', requestId);
|
|
|
|
if (error) {
|
|
return interaction.reply({ content: 'Failed to accept partnership.', ephemeral: true });
|
|
}
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(0x00ff00)
|
|
.setTitle('Partnership Accepted!')
|
|
.setDescription(`You are now partners with **${request.requester_guild_name}**!`)
|
|
.addFields(
|
|
{ name: 'Partner Server ID', value: `\`${request.requester_guild_id}\``, inline: true }
|
|
)
|
|
.setTimestamp();
|
|
|
|
await interaction.reply({ embeds: [embed] });
|
|
|
|
const notifyEmbed = new EmbedBuilder()
|
|
.setColor(0x00ff00)
|
|
.setTitle('Partnership Request Accepted!')
|
|
.setDescription(`**${interaction.guild.name}** has accepted your partnership request!`)
|
|
.setTimestamp();
|
|
|
|
await notifyPartnerServer(client, supabase, request.requester_guild_id, notifyEmbed);
|
|
}
|
|
|
|
if (subcommand === 'reject') {
|
|
const requestId = interaction.options.getString('request_id');
|
|
const reason = interaction.options.getString('reason') || 'No reason provided';
|
|
|
|
const { data: request } = await supabase
|
|
.from('server_partnerships')
|
|
.select('*')
|
|
.eq('id', requestId)
|
|
.eq('target_guild_id', interaction.guildId)
|
|
.eq('status', 'pending')
|
|
.maybeSingle();
|
|
|
|
if (!request) {
|
|
return interaction.reply({ content: 'Partnership request not found or not pending.', ephemeral: true });
|
|
}
|
|
|
|
const { error } = await supabase
|
|
.from('server_partnerships')
|
|
.update({
|
|
status: 'rejected',
|
|
rejection_reason: reason,
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', requestId);
|
|
|
|
if (error) {
|
|
return interaction.reply({ content: 'Failed to reject partnership.', ephemeral: true });
|
|
}
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(0xff6600)
|
|
.setTitle('Partnership Rejected')
|
|
.setDescription(`Request from **${request.requester_guild_name}** has been rejected.`)
|
|
.addFields({ name: 'Reason', value: reason })
|
|
.setTimestamp();
|
|
|
|
await interaction.reply({ embeds: [embed] });
|
|
|
|
const notifyEmbed = new EmbedBuilder()
|
|
.setColor(0xff6600)
|
|
.setTitle('Partnership Request Declined')
|
|
.setDescription(`**${interaction.guild.name}** has declined your partnership request.`)
|
|
.addFields({ name: 'Reason', value: reason })
|
|
.setTimestamp();
|
|
|
|
await notifyPartnerServer(client, supabase, request.requester_guild_id, notifyEmbed);
|
|
}
|
|
|
|
if (subcommand === 'end') {
|
|
const partnerId = interaction.options.getString('partner_id');
|
|
|
|
const { data: partnership } = await supabase
|
|
.from('server_partnerships')
|
|
.select('*')
|
|
.eq('status', 'accepted')
|
|
.or(`and(requester_guild_id.eq.${interaction.guildId},target_guild_id.eq.${partnerId}),and(requester_guild_id.eq.${partnerId},target_guild_id.eq.${interaction.guildId})`)
|
|
.maybeSingle();
|
|
|
|
if (!partnership) {
|
|
return interaction.reply({ content: 'No active partnership found with this server.', ephemeral: true });
|
|
}
|
|
|
|
const partnerName = partnership.requester_guild_id === interaction.guildId
|
|
? partnership.target_guild_name
|
|
: partnership.requester_guild_name;
|
|
|
|
const { error } = await supabase
|
|
.from('server_partnerships')
|
|
.update({
|
|
status: 'ended',
|
|
ended_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', partnership.id);
|
|
|
|
if (error) {
|
|
return interaction.reply({ content: 'Failed to end partnership.', ephemeral: true });
|
|
}
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(0xff0000)
|
|
.setTitle('Partnership Ended')
|
|
.setDescription(`Your partnership with **${partnerName}** has been ended.`)
|
|
.setTimestamp();
|
|
|
|
await interaction.reply({ embeds: [embed] });
|
|
|
|
const notifyEmbed = new EmbedBuilder()
|
|
.setColor(0xff0000)
|
|
.setTitle('Partnership Ended')
|
|
.setDescription(`**${interaction.guild.name}** has ended their partnership with your server.`)
|
|
.setTimestamp();
|
|
|
|
await notifyPartnerServer(client, supabase, partnerId, notifyEmbed);
|
|
}
|
|
}
|