Implements a new backup command with subcommands for creating, listing, viewing, restoring, deleting, configuring auto-backups, and comparing server states. Adds API endpoints for managing backups and integrates backup features into the dashboard UI. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 5b902c89-a0d5-43f0-923c-324d0f4e4e7d Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/TibkBZT Replit-Helium-Checkpoint-Created: true
677 lines
24 KiB
JavaScript
677 lines
24 KiB
JavaScript
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:<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: `<t:${Math.floor(new Date(backup.created_at).getTime() / 1000)}:R>`, 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: `<t:${Math.floor(new Date(backup.created_at).getTime() / 1000)}:F>`, 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 <t:${Math.floor(new Date(backup.created_at).getTime() / 1000)}:R>`)
|
||
.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;
|