AeThex-Bot-Master/aethex-bot/commands/backup.js
sirpiglr 03c30058f1 Add server backup and restore functionality for administrators
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
2025-12-13 04:15:06 +00:00

677 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;