diff --git a/aethex-bot/commands/federation.js b/aethex-bot/commands/federation.js index 504c077..740faaf 100644 --- a/aethex-bot/commands/federation.js +++ b/aethex-bot/commands/federation.js @@ -69,6 +69,23 @@ module.exports = { .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) { @@ -94,6 +111,8 @@ module.exports = { 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); } }, }; @@ -694,3 +713,323 @@ async function createBanAlerts(supabase, client, userId, severity) { 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 ` - Accept a request\n' + + '`/federation partners reject ` - 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); + } +}