const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('backup') .setDescription('Server backup and restore system') .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .addSubcommand(subcommand => subcommand .setName('create') .setDescription('Create a backup of server configuration') .addStringOption(option => option.setName('name') .setDescription('Name for this backup') .setRequired(false) ) .addStringOption(option => option.setName('description') .setDescription('Description of this backup') .setRequired(false) ) ) .addSubcommand(subcommand => subcommand .setName('list') .setDescription('List all available backups') ) .addSubcommand(subcommand => subcommand .setName('view') .setDescription('View details of a specific backup') .addStringOption(option => option.setName('id') .setDescription('Backup ID to view') .setRequired(true) ) ) .addSubcommand(subcommand => subcommand .setName('restore') .setDescription('Restore from a backup') .addStringOption(option => option.setName('id') .setDescription('Backup ID to restore') .setRequired(true) ) .addStringOption(option => option.setName('components') .setDescription('What to restore') .setRequired(false) .addChoices( { name: 'Everything', value: 'all' }, { name: 'Roles Only', value: 'roles' }, { name: 'Channels Only', value: 'channels' }, { name: 'Bot Config Only', value: 'bot_config' } ) ) ) .addSubcommand(subcommand => subcommand .setName('delete') .setDescription('Delete a backup') .addStringOption(option => option.setName('id') .setDescription('Backup ID to delete') .setRequired(true) ) ) .addSubcommand(subcommand => subcommand .setName('auto') .setDescription('Configure automatic backups') .addBooleanOption(option => option.setName('enabled') .setDescription('Enable or disable auto backups') .setRequired(true) ) .addIntegerOption(option => option.setName('interval') .setDescription('Backup interval in hours (default: 24)') .setRequired(false) .setMinValue(1) .setMaxValue(168) ) .addIntegerOption(option => option.setName('keep') .setDescription('Number of backups to keep (default: 7)') .setRequired(false) .setMinValue(1) .setMaxValue(30) ) ) .addSubcommand(subcommand => subcommand .setName('compare') .setDescription('Compare current server state with a backup') .addStringOption(option => option.setName('id') .setDescription('Backup ID to compare against') .setRequired(true) ) ), async execute(interaction, supabase, client) { const subcommand = interaction.options.getSubcommand(); if (!supabase) { const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle('Database Not Connected') .setDescription('Backup system requires database connection.') .setTimestamp(); return interaction.reply({ embeds: [embed], ephemeral: true }); } if (subcommand === 'create') { await interaction.deferReply(); const backupName = interaction.options.getString('name') || `Backup ${new Date().toLocaleDateString()}`; const description = interaction.options.getString('description') || 'Manual backup'; try { const backupData = await createBackup(interaction.guild, supabase, interaction.guildId); const { data: backup, error } = await supabase.from('server_backups').insert({ guild_id: interaction.guildId, name: backupName, description: description, backup_type: 'manual', created_by: interaction.user.id, data: backupData, roles_count: backupData.roles?.length || 0, channels_count: backupData.channels?.length || 0, size_bytes: JSON.stringify(backupData).length }).select().single(); if (error) throw error; const embed = new EmbedBuilder() .setColor(0x00ff00) .setTitle('Backup Created Successfully') .setDescription(`Your server configuration has been saved.`) .addFields( { name: 'Backup ID', value: `\`${backup.id}\``, inline: true }, { name: 'Name', value: backupName, inline: true }, { name: 'Created By', value: `<@${interaction.user.id}>`, inline: true }, { name: 'Roles Saved', value: `${backupData.roles?.length || 0}`, inline: true }, { name: 'Channels Saved', value: `${backupData.channels?.length || 0}`, inline: true }, { name: 'Size', value: formatBytes(backup.size_bytes), inline: true } ) .setFooter({ text: `Use /backup restore id:${backup.id} to restore` }) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); } catch (error) { console.error('Backup creation error:', error); const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle('Backup Failed') .setDescription(`Failed to create backup: ${error.message}`) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); } } else if (subcommand === 'list') { try { const { data: backups, error } = await supabase .from('server_backups') .select('id, name, description, backup_type, created_at, created_by, roles_count, channels_count, size_bytes') .eq('guild_id', interaction.guildId) .order('created_at', { ascending: false }) .limit(15); if (error) throw error; if (!backups || backups.length === 0) { const embed = new EmbedBuilder() .setColor(0xffa500) .setTitle('No Backups Found') .setDescription('This server has no backups. Create one with `/backup create`') .setTimestamp(); return interaction.reply({ embeds: [embed] }); } const backupList = backups.map((b, i) => { const date = new Date(b.created_at).toLocaleDateString(); const typeIcon = b.backup_type === 'auto' ? '🔄' : '📁'; return `${typeIcon} **${b.name}**\n> ID: \`${b.id}\` | ${date} | ${b.roles_count} roles, ${b.channels_count} channels`; }).join('\n\n'); const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle(`Server Backups (${backups.length})`) .setDescription(backupList) .setFooter({ text: 'Use /backup view id: for details' }) .setTimestamp(); await interaction.reply({ embeds: [embed] }); } catch (error) { console.error('List backups error:', error); const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle('Error') .setDescription('Failed to list backups.') .setTimestamp(); await interaction.reply({ embeds: [embed], ephemeral: true }); } } else if (subcommand === 'view') { const backupId = interaction.options.getString('id'); try { const { data: backup, error } = await supabase .from('server_backups') .select('*') .eq('id', backupId) .eq('guild_id', interaction.guildId) .single(); if (error || !backup) { const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle('Backup Not Found') .setDescription('Could not find a backup with that ID for this server.') .setTimestamp(); return interaction.reply({ embeds: [embed], ephemeral: true }); } const data = backup.data; const rolesPreview = data.roles?.slice(0, 5).map(r => r.name).join(', ') || 'None'; const channelsPreview = data.channels?.slice(0, 5).map(c => `#${c.name}`).join(', ') || 'None'; const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle(`Backup: ${backup.name}`) .setDescription(backup.description || 'No description') .addFields( { name: 'Backup ID', value: `\`${backup.id}\``, inline: true }, { name: 'Type', value: backup.backup_type === 'auto' ? '🔄 Automatic' : '📁 Manual', inline: true }, { name: 'Created', value: ``, inline: true }, { name: 'Created By', value: backup.created_by ? `<@${backup.created_by}>` : 'System', inline: true }, { name: 'Size', value: formatBytes(backup.size_bytes), inline: true }, { name: '\u200b', value: '\u200b', inline: true }, { name: `Roles (${backup.roles_count})`, value: rolesPreview + (data.roles?.length > 5 ? '...' : ''), inline: false }, { name: `Channels (${backup.channels_count})`, value: channelsPreview + (data.channels?.length > 5 ? '...' : ''), inline: false } ) .setFooter({ text: `Use /backup restore id:${backup.id} to restore` }) .setTimestamp(); const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId(`backup_restore_${backup.id}`) .setLabel('Restore This Backup') .setStyle(ButtonStyle.Primary) .setEmoji('🔄'), new ButtonBuilder() .setCustomId(`backup_delete_${backup.id}`) .setLabel('Delete') .setStyle(ButtonStyle.Danger) .setEmoji('🗑️') ); await interaction.reply({ embeds: [embed], components: [row] }); } catch (error) { console.error('View backup error:', error); const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle('Error') .setDescription('Failed to view backup.') .setTimestamp(); await interaction.reply({ embeds: [embed], ephemeral: true }); } } else if (subcommand === 'restore') { const backupId = interaction.options.getString('id'); const components = interaction.options.getString('components') || 'all'; try { const { data: backup, error } = await supabase .from('server_backups') .select('*') .eq('id', backupId) .eq('guild_id', interaction.guildId) .single(); if (error || !backup) { const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle('Backup Not Found') .setDescription('Could not find a backup with that ID for this server.') .setTimestamp(); return interaction.reply({ embeds: [embed], ephemeral: true }); } const confirmEmbed = new EmbedBuilder() .setColor(0xffa500) .setTitle('⚠️ Confirm Restore') .setDescription(`Are you sure you want to restore from **${backup.name}**?\n\nThis will restore: **${components === 'all' ? 'Everything' : components}**\n\n**Warning:** This action may modify your server's roles and channels. A new backup will be created first.`) .addFields( { name: 'Backup Date', value: ``, inline: true }, { name: 'Components', value: components, inline: true } ) .setTimestamp(); const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId(`backup_confirm_restore_${backupId}_${components}`) .setLabel('Yes, Restore') .setStyle(ButtonStyle.Danger) .setEmoji('✅'), new ButtonBuilder() .setCustomId('backup_cancel_restore') .setLabel('Cancel') .setStyle(ButtonStyle.Secondary) ); await interaction.reply({ embeds: [confirmEmbed], components: [row], ephemeral: true }); } catch (error) { console.error('Restore backup error:', error); const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle('Error') .setDescription('Failed to initiate restore.') .setTimestamp(); await interaction.reply({ embeds: [embed], ephemeral: true }); } } else if (subcommand === 'delete') { const backupId = interaction.options.getString('id'); try { const { data: backup, error: fetchError } = await supabase .from('server_backups') .select('id, name') .eq('id', backupId) .eq('guild_id', interaction.guildId) .single(); if (fetchError || !backup) { const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle('Backup Not Found') .setDescription('Could not find a backup with that ID for this server.') .setTimestamp(); return interaction.reply({ embeds: [embed], ephemeral: true }); } const { error: deleteError } = await supabase .from('server_backups') .delete() .eq('id', backupId); if (deleteError) throw deleteError; const embed = new EmbedBuilder() .setColor(0x00ff00) .setTitle('Backup Deleted') .setDescription(`Successfully deleted backup: **${backup.name}**`) .setTimestamp(); await interaction.reply({ embeds: [embed] }); } catch (error) { console.error('Delete backup error:', error); const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle('Error') .setDescription('Failed to delete backup.') .setTimestamp(); await interaction.reply({ embeds: [embed], ephemeral: true }); } } else if (subcommand === 'auto') { const enabled = interaction.options.getBoolean('enabled'); const interval = interaction.options.getInteger('interval') || 24; const keep = interaction.options.getInteger('keep') || 7; try { await supabase.from('backup_settings').upsert({ guild_id: interaction.guildId, auto_enabled: enabled, interval_hours: interval, max_backups: keep, updated_at: new Date().toISOString(), updated_by: interaction.user.id }); const embed = new EmbedBuilder() .setColor(enabled ? 0x00ff00 : 0xffa500) .setTitle(`Auto Backups ${enabled ? 'Enabled' : 'Disabled'}`) .setDescription(enabled ? `Automatic backups are now **enabled** for this server.` : `Automatic backups have been **disabled**.` ) .setTimestamp(); if (enabled) { embed.addFields( { name: 'Interval', value: `Every ${interval} hour${interval === 1 ? '' : 's'}`, inline: true }, { name: 'Backups Kept', value: `${keep}`, inline: true } ); } await interaction.reply({ embeds: [embed] }); } catch (error) { console.error('Auto backup settings error:', error); const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle('Error') .setDescription('Failed to update auto backup settings.') .setTimestamp(); await interaction.reply({ embeds: [embed], ephemeral: true }); } } else if (subcommand === 'compare') { await interaction.deferReply(); const backupId = interaction.options.getString('id'); try { const { data: backup, error } = await supabase .from('server_backups') .select('*') .eq('id', backupId) .eq('guild_id', interaction.guildId) .single(); if (error || !backup) { const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle('Backup Not Found') .setDescription('Could not find a backup with that ID for this server.') .setTimestamp(); return interaction.editReply({ embeds: [embed] }); } const currentData = await createBackup(interaction.guild, supabase, interaction.guildId); const backupData = backup.data; const currentRoleNames = new Set(currentData.roles.map(r => r.name)); const backupRoleNames = new Set(backupData.roles?.map(r => r.name) || []); const addedRoles = currentData.roles.filter(r => !backupRoleNames.has(r.name)).map(r => r.name); const removedRoles = (backupData.roles || []).filter(r => !currentRoleNames.has(r.name)).map(r => r.name); const currentChannelNames = new Set(currentData.channels.map(c => c.name)); const backupChannelNames = new Set(backupData.channels?.map(c => c.name) || []); const addedChannels = currentData.channels.filter(c => !backupChannelNames.has(c.name)).map(c => `#${c.name}`); const removedChannels = (backupData.channels || []).filter(c => !currentChannelNames.has(c.name)).map(c => `#${c.name}`); const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle(`Comparison: ${backup.name}`) .setDescription(`Comparing current server state against backup from `) .addFields( { name: '➕ Roles Added Since Backup', value: addedRoles.length > 0 ? addedRoles.slice(0, 10).join(', ') + (addedRoles.length > 10 ? '...' : '') : 'None', inline: false }, { name: '➖ Roles Removed Since Backup', value: removedRoles.length > 0 ? removedRoles.slice(0, 10).join(', ') + (removedRoles.length > 10 ? '...' : '') : 'None', inline: false }, { name: '➕ Channels Added', value: addedChannels.length > 0 ? addedChannels.slice(0, 10).join(', ') + (addedChannels.length > 10 ? '...' : '') : 'None', inline: false }, { name: '➖ Channels Removed', value: removedChannels.length > 0 ? removedChannels.slice(0, 10).join(', ') + (removedChannels.length > 10 ? '...' : '') : 'None', inline: false } ) .setFooter({ text: 'Note: Permission and setting changes are not shown in this comparison' }) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); } catch (error) { console.error('Compare backup error:', error); const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle('Error') .setDescription('Failed to compare backup.') .setTimestamp(); await interaction.editReply({ embeds: [embed] }); } } } }; async function createBackup(guild, supabase, guildId) { const roles = guild.roles.cache .filter(r => !r.managed && r.id !== guild.id) .map(role => ({ id: role.id, name: role.name, color: role.color, hoist: role.hoist, position: role.position, permissions: role.permissions.bitfield.toString(), mentionable: role.mentionable })) .sort((a, b) => b.position - a.position); const channels = guild.channels.cache .filter(c => c.type !== 4) .map(channel => ({ id: channel.id, name: channel.name, type: channel.type, position: channel.position, parentId: channel.parentId, topic: channel.topic || null, nsfw: channel.nsfw || false, rateLimitPerUser: channel.rateLimitPerUser || 0, permissionOverwrites: channel.permissionOverwrites?.cache.map(po => ({ id: po.id, type: po.type, allow: po.allow.bitfield.toString(), deny: po.deny.bitfield.toString() })) || [] })) .sort((a, b) => a.position - b.position); const categories = guild.channels.cache .filter(c => c.type === 4) .map(cat => ({ id: cat.id, name: cat.name, position: cat.position, permissionOverwrites: cat.permissionOverwrites?.cache.map(po => ({ id: po.id, type: po.type, allow: po.allow.bitfield.toString(), deny: po.deny.bitfield.toString() })) || [] })) .sort((a, b) => a.position - b.position); let botConfig = null; if (supabase) { const { data } = await supabase .from('server_config') .select('*') .eq('guild_id', guildId) .single(); botConfig = data; } let levelRoles = []; if (supabase) { const { data } = await supabase .from('level_roles') .select('*') .eq('guild_id', guildId); levelRoles = data || []; } let automodConfig = null; if (supabase) { const { data } = await supabase .from('automod_config') .select('*') .eq('guild_id', guildId) .single(); automodConfig = data; } return { guildInfo: { name: guild.name, icon: guild.iconURL(), ownerId: guild.ownerId, memberCount: guild.memberCount }, roles, channels, categories, botConfig, levelRoles, automodConfig, backupVersion: '1.0' }; } async function performRestore(guild, backupData, components, supabase) { const results = { roles: { created: 0, failed: 0, skipped: 0 }, channels: { created: 0, failed: 0, skipped: 0 }, botConfig: false }; if (components === 'all' || components === 'roles') { const existingRoles = new Set(guild.roles.cache.map(r => r.name)); for (const roleData of backupData.roles || []) { if (existingRoles.has(roleData.name)) { results.roles.skipped++; continue; } try { await guild.roles.create({ name: roleData.name, color: roleData.color, hoist: roleData.hoist, permissions: BigInt(roleData.permissions), mentionable: roleData.mentionable, reason: 'Restored from backup' }); results.roles.created++; } catch (e) { results.roles.failed++; } } } if (components === 'all' || components === 'channels') { const existingChannels = new Set(guild.channels.cache.map(c => c.name)); for (const catData of backupData.categories || []) { if (existingChannels.has(catData.name)) continue; try { await guild.channels.create({ name: catData.name, type: 4, reason: 'Restored from backup' }); } catch (e) { console.error('Failed to create category:', e.message); } } for (const channelData of backupData.channels || []) { if (existingChannels.has(channelData.name)) { results.channels.skipped++; continue; } try { const parent = channelData.parentId ? guild.channels.cache.find(c => c.name === (backupData.categories?.find(cat => cat.id === channelData.parentId)?.name)) : null; await guild.channels.create({ name: channelData.name, type: channelData.type, topic: channelData.topic, nsfw: channelData.nsfw, rateLimitPerUser: channelData.rateLimitPerUser, parent: parent?.id, reason: 'Restored from backup' }); results.channels.created++; } catch (e) { results.channels.failed++; } } } if ((components === 'all' || components === 'bot_config') && supabase && backupData.botConfig) { try { await supabase.from('server_config').upsert({ ...backupData.botConfig, guild_id: guild.id, updated_at: new Date().toISOString() }); results.botConfig = true; } catch (e) { console.error('Failed to restore bot config:', e.message); } } return results; } function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } module.exports.createBackup = createBackup; module.exports.performRestore = performRestore;