discord-backup
Version:
A complete framework to facilitate server backup using discord.js v14 with rate limiting and error handling
420 lines (419 loc) • 17.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchChannelPermissions = fetchChannelPermissions;
exports.fetchVoiceChannelData = fetchVoiceChannelData;
exports.fetchChannelMessages = fetchChannelMessages;
exports.fetchTextChannelData = fetchTextChannelData;
exports.loadCategory = loadCategory;
exports.loadChannel = loadChannel;
exports.clearGuild = clearGuild;
const discord_js_1 = require("discord.js");
const node_fetch_1 = require("node-fetch");
const MaxBitratePerTier = {
[discord_js_1.GuildPremiumTier.None]: 64000,
[discord_js_1.GuildPremiumTier.Tier1]: 128000,
[discord_js_1.GuildPremiumTier.Tier2]: 256000,
[discord_js_1.GuildPremiumTier.Tier3]: 384000
};
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function withRetry(operation, maxRetries = 3, baseDelay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
}
catch (error) {
if (attempt === maxRetries) {
throw error;
}
const delayMs = baseDelay * Math.pow(2, attempt - 1);
if (error?.code === 50013) {
throw error;
}
if (error?.code === 50035 || error?.code === 50001) {
throw error;
}
if (error?.status === 429) {
const retryAfter = error?.retryAfter ? error.retryAfter * 1000 : delayMs;
// Rate limited - waiting before retry
await delay(retryAfter);
continue;
}
// Operation failed - retrying
await delay(delayMs);
}
}
throw new Error('Max retries exceeded');
}
/**
* Gets the permissions for a channel
*/
function fetchChannelPermissions(channel) {
const permissions = [];
channel.permissionOverwrites.cache
.filter((p) => p.type === discord_js_1.OverwriteType.Role)
.forEach((perm) => {
// For each overwrites permission
const role = channel.guild.roles.cache.get(perm.id);
if (role) {
permissions.push({
roleName: role.name,
allow: perm.allow.bitfield.toString(),
deny: perm.deny.bitfield.toString()
});
}
});
return permissions;
}
/**
* Fetches the voice channel data that is necessary for the backup
*/
async function fetchVoiceChannelData(channel) {
return new Promise(async (resolve) => {
const channelData = {
type: discord_js_1.ChannelType.GuildVoice,
name: channel.name,
bitrate: channel.bitrate,
userLimit: channel.userLimit,
parent: channel.parent ? channel.parent.name : null,
permissions: fetchChannelPermissions(channel)
};
/* Return channel data */
resolve(channelData);
});
}
async function fetchChannelMessages(channel, options) {
const messages = [];
const messageCount = isNaN(options.maxMessagesPerChannel)
? 10
: Math.min(options.maxMessagesPerChannel, 1000);
const fetchOptions = { limit: Math.min(100, messageCount) };
let lastMessageId;
let fetchComplete = false;
while (!fetchComplete && messages.length < messageCount) {
try {
if (lastMessageId) {
fetchOptions.before = lastMessageId;
}
const fetched = await withRetry(() => channel.messages.fetch(fetchOptions), 3, 1000);
if (fetched.size === 0) {
break;
}
lastMessageId = fetched.last().id;
for (const msg of fetched.values()) {
if (!msg.author || messages.length >= messageCount) {
fetchComplete = true;
break;
}
try {
const files = await Promise.all(msg.attachments.map(async (a) => {
let attach = a.url;
const fileExt = a.url.split('.').pop()?.toLowerCase();
if (a.url &&
fileExt &&
['png', 'jpg', 'jpeg', 'jpe', 'jif', 'jfif', 'jfi', 'gif', 'webp'].includes(fileExt)) {
if (options.saveImages && options.saveImages === 'base64') {
try {
const response = await withRetry(() => (0, node_fetch_1.default)(a.url), 2, 500);
const buffer = await response.buffer();
if (buffer.length <= 8 * 1024 * 1024) {
attach = buffer.toString('base64');
}
}
catch (error) {
// Failed to fetch attachment - using URL instead
}
}
}
return {
name: a.name,
attachment: attach
};
}));
messages.push({
username: msg.author.username,
avatar: msg.author.displayAvatarURL(),
content: msg.cleanContent || '',
embeds: msg.embeds.slice(0, 10).map((embed) => ({
title: embed.title,
description: embed.description,
url: embed.url,
color: embed.color,
timestamp: embed.timestamp,
fields: embed.fields?.slice(0, 25),
author: embed.author,
footer: embed.footer,
thumbnail: embed.thumbnail,
image: embed.image
})),
files,
pinned: msg.pinned,
sentAt: msg.createdAt.toISOString()
});
}
catch (error) {
// Failed to process message - skipping
}
}
await delay(100);
}
catch (error) {
// Failed to fetch messages - stopping
break;
}
}
return messages;
}
/**
* Fetches the text channel data that is necessary for the backup
*/
async function fetchTextChannelData(channel, options) {
return new Promise(async (resolve) => {
const channelData = {
type: channel.type,
name: channel.name,
nsfw: channel.nsfw,
rateLimitPerUser: channel.type === discord_js_1.ChannelType.GuildText ? channel.rateLimitPerUser : undefined,
parent: channel.parent ? channel.parent.name : null,
topic: channel.topic,
permissions: fetchChannelPermissions(channel),
messages: [],
isNews: channel.type === discord_js_1.ChannelType.GuildAnnouncement,
threads: []
};
/* Fetch channel threads */
if (channel.threads.cache.size > 0) {
await Promise.all(channel.threads.cache.map(async (thread) => {
const threadData = {
type: thread.type,
name: thread.name,
archived: thread.archived,
autoArchiveDuration: thread.autoArchiveDuration,
locked: thread.locked,
rateLimitPerUser: thread.rateLimitPerUser,
messages: []
};
try {
threadData.messages = await fetchChannelMessages(thread, options);
/* Return thread data */
channelData.threads.push(threadData);
}
catch {
channelData.threads.push(threadData);
}
}));
}
/* Fetch channel messages */
try {
channelData.messages = await fetchChannelMessages(channel, options);
/* Return channel data */
resolve(channelData);
}
catch {
resolve(channelData);
}
});
}
/**
* Creates a category for the guild
*/
async function loadCategory(categoryData, guild) {
return withRetry(async () => {
const category = await guild.channels.create({
name: categoryData.name,
type: discord_js_1.ChannelType.GuildCategory
});
const finalPermissions = [];
categoryData.permissions.forEach((perm) => {
const role = guild.roles.cache.find((r) => r.name === perm.roleName);
if (role) {
finalPermissions.push({
id: role.id,
allow: BigInt(perm.allow),
deny: BigInt(perm.deny)
});
}
});
if (finalPermissions.length > 0) {
await withRetry(() => category.permissionOverwrites.set(finalPermissions));
}
return category;
}, 3, 2000);
}
/**
* Create a channel and returns it
*/
async function loadChannel(channelData, guild, category, options) {
return new Promise(async (resolve) => {
const loadMessages = async (channel, messages, previousWebhook) => {
try {
const webhook = previousWebhook ||
(await withRetry(() => channel.createWebhook({
name: 'MessagesBackup',
avatar: channel.client.user.displayAvatarURL()
}), 2, 1000).catch(() => null));
if (!webhook)
return;
const filteredMessages = messages
.filter((m) => m.content?.length > 0 || m.embeds?.length > 0 || m.files?.length > 0)
.reverse()
.slice(0, options?.maxMessagesPerChannel || 10);
for (let i = 0; i < filteredMessages.length; i++) {
const msg = filteredMessages[i];
try {
const files = msg.files
?.map((f) => {
try {
return new discord_js_1.AttachmentBuilder(f.attachment, { name: f.name });
}
catch {
return null;
}
})
.filter((f) => f !== null) || [];
const sentMsg = await withRetry(() => webhook.send({
content: msg.content?.length ? msg.content.slice(0, 2000) : undefined,
username: msg.username?.slice(0, 80) || 'Unknown User',
avatarURL: msg.avatar,
embeds: msg.embeds?.slice(0, 10) || [],
files: files.slice(0, 10),
allowedMentions: options?.allowedMentions || { parse: [] },
threadId: channel.isThread() ? channel.id : undefined
}), 2, 500).catch((error) => {
// Failed to send message
return null;
});
if (msg.pinned && sentMsg) {
await withRetry(() => sentMsg.pin(), 1, 1000).catch(() => { });
}
if (i < filteredMessages.length - 1) {
await delay(1000);
}
}
catch (error) {
// Failed to process message
}
}
return webhook;
}
catch (error) {
// Failed to load messages
return;
}
};
const createOptions = {
name: channelData.name,
type: null,
parent: category
};
if (channelData.type === discord_js_1.ChannelType.GuildText || channelData.type === discord_js_1.ChannelType.GuildAnnouncement) {
createOptions.topic = channelData.topic;
createOptions.nsfw = channelData.nsfw;
createOptions.rateLimitPerUser = channelData.rateLimitPerUser;
createOptions.type =
channelData.isNews && guild.features.includes(discord_js_1.GuildFeature.News)
? discord_js_1.ChannelType.GuildAnnouncement
: discord_js_1.ChannelType.GuildText;
}
else if (channelData.type === discord_js_1.ChannelType.GuildVoice) {
// Downgrade bitrate
let bitrate = channelData.bitrate;
const bitrates = Object.values(MaxBitratePerTier);
while (bitrate > MaxBitratePerTier[guild.premiumTier]) {
bitrate = bitrates[guild.premiumTier];
}
createOptions.bitrate = bitrate;
createOptions.userLimit = channelData.userLimit;
createOptions.type = discord_js_1.ChannelType.GuildVoice;
}
guild.channels.create(createOptions).then(async (channel) => {
/* Update channel permissions */
const finalPermissions = [];
channelData.permissions.forEach((perm) => {
const role = guild.roles.cache.find((r) => r.name === perm.roleName);
if (role) {
finalPermissions.push({
id: role.id,
allow: BigInt(perm.allow),
deny: BigInt(perm.deny)
});
}
});
await channel.permissionOverwrites.set(finalPermissions);
if (channelData.type === discord_js_1.ChannelType.GuildText) {
/* Load messages */
let webhook;
if (channelData.messages.length > 0) {
webhook = await loadMessages(channel, channelData.messages).catch(() => { });
}
/* Load threads */
if (channelData.threads.length > 0) {
// && guild.features.includes('THREADS_ENABLED')) {
await Promise.all(channelData.threads.map(async (threadData) => {
const autoArchiveDuration = threadData.autoArchiveDuration;
// if (!guild.features.includes('SEVEN_DAY_THREAD_ARCHIVE') && autoArchiveDuration === 10080) autoArchiveDuration = 4320;
// if (!guild.features.includes('THREE_DAY_THREAD_ARCHIVE') && autoArchiveDuration === 4320) autoArchiveDuration = 1440;
return channel.threads
.create({
name: threadData.name,
autoArchiveDuration
})
.then((thread) => {
if (!webhook)
return;
return loadMessages(thread, threadData.messages, webhook);
});
}));
}
return channel;
}
else {
resolve(channel); // Return the channel
}
});
});
}
/**
* Delete all roles, all channels, all emojis, etc... of a guild
*/
async function clearGuild(guild) {
guild.roles.cache
.filter((role) => !role.managed && role.editable && role.id !== guild.id)
.forEach((role) => {
role.delete().catch(() => { });
});
guild.channels.cache.forEach((channel) => {
channel.delete().catch(() => { });
});
guild.emojis.cache.forEach((emoji) => {
emoji.delete().catch(() => { });
});
const webhooks = await guild.fetchWebhooks();
webhooks.forEach((webhook) => {
webhook.delete().catch(() => { });
});
const bans = await guild.bans.fetch();
bans.forEach((ban) => {
guild.members.unban(ban.user).catch(() => { });
});
guild.setAFKChannel(null);
guild.setAFKTimeout(60 * 5);
guild.setIcon(null);
guild.setBanner(null).catch(() => { });
guild.setSplash(null).catch(() => { });
guild.setDefaultMessageNotifications(discord_js_1.GuildDefaultMessageNotifications.OnlyMentions);
guild.setWidgetSettings({
enabled: false,
channel: null
});
if (!guild.features.includes(discord_js_1.GuildFeature.Community)) {
guild.setExplicitContentFilter(discord_js_1.GuildExplicitContentFilter.Disabled);
guild.setVerificationLevel(discord_js_1.GuildVerificationLevel.None);
}
guild.setSystemChannel(null);
guild.setSystemChannelFlags([
discord_js_1.GuildSystemChannelFlags.SuppressGuildReminderNotifications,
discord_js_1.GuildSystemChannelFlags.SuppressJoinNotifications,
discord_js_1.GuildSystemChannelFlags.SuppressPremiumSubscriptions
]);
return;
}