feat: Add purge message command

This commit is contained in:
2024-03-05 10:16:13 +07:00
parent 910a7d6543
commit c60ddc7086
2 changed files with 216 additions and 80 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ model Guild {
ServiceAnnouncement_Enabled Boolean @default(false) ServiceAnnouncement_Enabled Boolean @default(false)
ServiceAnnouncement_Channel String? ServiceAnnouncement_Channel String?
Purge_Limit Int @default(200) Purge_Limit Int @default(300)
} }
model User { model User {
+215 -79
View File
@@ -4,7 +4,8 @@ import {
PermissionsBitField, PermissionsBitField,
GuildBasedChannel, GuildBasedChannel,
BaseGuildTextChannel, BaseGuildTextChannel,
BaseInteraction BaseInteraction,
GuildTextBasedChannel
} from 'discord.js'; } from 'discord.js';
import { I18n } from 'i18n'; import { I18n } from 'i18n';
@@ -20,6 +21,11 @@ import Locale from '../services/Locale';
import Logger from '../libs/Logger'; import Logger from '../libs/Logger';
import Prisma from '../providers/Prisma'; import Prisma from '../providers/Prisma';
import Environment from '../providers/Environment'; import Environment from '../providers/Environment';
import {
checkBotPermissionsInChannel,
checkMemberPermissionsInChannel,
checkMemberPermissionsInGuild
} from '../utils/DiscordPermission';
const EMBEDS = { const EMBEDS = {
PURGE_INFO: (data: HybridInteractionMessage, locale: I18n) => { PURGE_INFO: (data: HybridInteractionMessage, locale: I18n) => {
@@ -35,19 +41,65 @@ const EMBEDS = {
user: data.getUser() user: data.getUser()
}); });
}, },
NO_PERMISSION: (data: HybridInteractionMessage, locale: I18n) => { PURGING_STAGE_1: (data: HybridInteractionMessage, locale: I18n, current: number, total: number) => {
return makeErrorEmbed({ return makeInfoEmbed({
title: locale.__('common.no_permissions'), title: locale.__('purge.title'),
description: locale.__('common.no_permissions_description', { description:
PERMISSIONS: 'VIEW_CHANNEL, MANAGE_MESSAGES' locale.__('purge.purging_progress', { CURRENT: current.toString(), TOTAL: total.toString() }) +
}), '\n\n' +
locale.__('purge.purging_step_bulk_delete_done', { COUNT: current.toString() }),
user: data.getUser() user: data.getUser()
}); });
}, },
PURGING: (data: HybridInteractionMessage, locale: I18n) => { PURGING_STAGE_2: (
data: HybridInteractionMessage,
locale: I18n,
current: number,
current_normal: number,
total: number
) => {
return makeInfoEmbed({ return makeInfoEmbed({
title: locale.__('purge.title'), title: locale.__('purge.title'),
description: locale.__('purge.purging'), description:
locale.__('purge.purging_progress', {
CURRENT: (current + current_normal).toString(),
TOTAL: total.toString()
}) +
'\n\n' +
locale.__('purge.purging_step_bulk_delete_done', { COUNT: current.toString() }) +
'\n' +
locale.__('purge.purging_step_normal_delete', { CURRENT: current_normal.toString() }),
user: data.getUser()
});
},
PURGING_DONE_EARLY: (data: HybridInteractionMessage, locale: I18n, current: number, total: number) => {
return makeSuccessEmbed({
title: locale.__('purge.title'),
description:
locale.__('purge.purge_progress', { COUNT: current.toString(), TOTAL: total.toString() }) +
'\n\n' +
locale.__('purge.purging_step_bulk_delete_done', { COUNT: current.toString() }),
user: data.getUser()
});
},
PURGING_DONE: (
data: HybridInteractionMessage,
locale: I18n,
current: number,
current_normal: number,
total: number
) => {
return makeSuccessEmbed({
title: locale.__('purge.title'),
description:
locale.__('purge.purge_progress', {
COUNT: (current + current_normal).toString(),
TOTAL: total.toString()
}) +
'\n\n' +
locale.__('purge.purging_step_bulk_delete_done', { COUNT: current.toString() }) +
'\n' +
locale.__('purge.purging_step_normal_delete_done', { COUNT: current_normal.toString() }),
user: data.getUser() user: data.getUser()
}); });
}, },
@@ -60,21 +112,21 @@ const EMBEDS = {
}, },
PURGE_NAN_ERROR: (data: HybridInteractionMessage, locale: I18n) => { PURGE_NAN_ERROR: (data: HybridInteractionMessage, locale: I18n) => {
return makeErrorEmbed({ return makeErrorEmbed({
title: locale.__('purge.error'), title: locale.__('purge.nan_error_title'),
description: locale.__('purge.error_nan'), description: locale.__('purge.nan_error_description'),
user: data.getUser() user: data.getUser()
}); });
}, },
PURGE_OVER_LIMIT: (data: HybridInteractionMessage, locale: I18n) => { PURGE_OVER_LIMIT: (data: HybridInteractionMessage, locale: I18n, limit: number) => {
return makeErrorEmbed({ return makeErrorEmbed({
title: locale.__('purge.error'), title: locale.__('common.member_no_permissions'),
description: locale.__('purge.error_over_limit'), description: locale.__('purge.error_over_limit', { MAX: limit.toString() }),
user: data.getUser() user: data.getUser()
}); });
}, },
SUCCESSFULLY_PURGED: (data: HybridInteractionMessage, locale: I18n) => { NOTHING_PURGED: (data: HybridInteractionMessage, locale: I18n) => {
return makeSuccessEmbed({ return makeSuccessEmbed({
title: locale.__('purge.success'), title: locale.__('purge.nothing_purged'),
user: data.getUser() user: data.getUser()
}); });
} }
@@ -94,34 +146,24 @@ export default class Purge extends DiscordModule {
} }
async run(data: HybridInteractionMessage, args: any) { async run(data: HybridInteractionMessage, args: any) {
// Disable this unfinished feature in production
if (Environment.get().NODE_ENV !== 'production') return;
const guild = data.getGuild(); const guild = data.getGuild();
if (!guild) return; if (!guild) return;
const member = data.getMember();
if (!member) return;
const channel = data.getGuildChannel() as GuildTextBasedChannel;
if (!channel) return;
const locale = await Locale.getGuildLocale(guild.id); const locale = await Locale.getGuildLocale(guild.id);
let query; let query;
const channel = data.getChannel();
if (!channel) return;
if (data.isMessage()) { if (data.isMessage()) {
const message = data.getMessage(); const message = data.getMessage();
if (!message.member) return; if (!message.member) return;
if (
!message.member.permissions.has([
PermissionsBitField.Flags.ViewChannel,
PermissionsBitField.Flags.ManageMessages
])
)
return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.NO_PERMISSION(data, locale)]
});
if (args.length === 0) if (args.length === 0)
return await sendHybridInteractionMessageResponse(data, { return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.PURGE_INFO(data, locale)] embeds: [EMBEDS.PURGE_INFO(data, locale)]
@@ -130,20 +172,6 @@ export default class Purge extends DiscordModule {
query = args.join(' '); query = args.join(' ');
} else if (data.isApplicationCommand()) { } else if (data.isApplicationCommand()) {
const interaction = data.getSlashCommand(); const interaction = data.getSlashCommand();
if (
!data
.getGuild()!
.members.cache.get(interaction.user.id)
?.permissions.has([PermissionsBitField.Flags.ViewChannel]) &&
data
.getGuild()!
.members.cache.get(interaction.user.id)
?.permissions.has([PermissionsBitField.Flags.ManageMessages])
)
return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.NO_PERMISSION(data, locale)]
});
query = interaction.options.get('message', true).value?.toString(); query = interaction.options.get('message', true).value?.toString();
} }
@@ -154,6 +182,28 @@ export default class Purge extends DiscordModule {
}); });
} }
if (
!(await checkMemberPermissionsInChannel({
member,
channel,
data,
locale,
permissions: [PermissionsBitField.Flags.ViewChannel, PermissionsBitField.Flags.ManageMessages]
}))
)
return;
if (
!(await checkBotPermissionsInChannel({
guild,
data,
locale,
channel,
permissions: [PermissionsBitField.Flags.ViewChannel, PermissionsBitField.Flags.ManageMessages]
}))
)
return;
const PrismaGuild = await Prisma.client.guild.findFirst({ where: { id: guild.id } }); const PrismaGuild = await Prisma.client.guild.findFirst({ where: { id: guild.id } });
if (!PrismaGuild) return; if (!PrismaGuild) return;
@@ -161,13 +211,15 @@ export default class Purge extends DiscordModule {
try { try {
let totalMessagesToDelete = parseInt(query); let totalMessagesToDelete = parseInt(query);
// if (totalMessagesToDelete > guildLimit) { if (totalMessagesToDelete > guildLimit) {
// return await sendHybridInteractionMessageResponse(data, { return await sendHybridInteractionMessageResponse(data, {
// embeds: [EMBEDS.PURGE_OVER_LIMIT(data, locale)] embeds: [EMBEDS.PURGE_OVER_LIMIT(data, locale, guildLimit)]
// }); });
// } }
let totalMessageDeleted = 0;
let totalBulkDeletedMessages = 0;
let amountToDelete = totalMessagesToDelete > 100 ? 100 : totalMessagesToDelete;
let messages; let messages;
let statusMessage; let statusMessage;
if (data.isMessage()) { if (data.isMessage()) {
@@ -178,20 +230,22 @@ export default class Purge extends DiscordModule {
.catch(() => {}); .catch(() => {});
statusMessage = await sendMessage(channel, data.getUser()!, { statusMessage = await sendMessage(channel, data.getUser()!, {
embeds: [EMBEDS.PURGING(data, locale)] embeds: [EMBEDS.PURGING_STAGE_1(data, locale, totalBulkDeletedMessages, totalMessagesToDelete)]
}); });
} else if (data.isApplicationCommand()) { } else if (data.isApplicationCommand()) {
statusMessage = await data statusMessage = await data.getMessageComponentInteraction().reply({
.getMessageComponentInteraction() embeds: [EMBEDS.PURGING_STAGE_1(data, locale, totalBulkDeletedMessages, totalMessagesToDelete)],
.reply({ embeds: [EMBEDS.PURGING(data, locale)], fetchReply: true }); fetchReply: true
});
statusMessage = statusMessage as unknown as BaseInteraction; statusMessage = statusMessage as unknown as BaseInteraction;
} }
statusMessage = new HybridInteractionMessage(statusMessage!); statusMessage = new HybridInteractionMessage(statusMessage!);
let amountToDelete = totalMessagesToDelete > 100 ? 100 : totalMessagesToDelete;
messages = await channel.messages.fetch({ messages = await channel.messages.fetch({
limit: amountToDelete, limit: amountToDelete,
before: statusMessage!.getMessage().id before: statusMessage.getMessage().id
}); });
// Filter out statusMessage // Filter out statusMessage
@@ -202,23 +256,70 @@ export default class Purge extends DiscordModule {
// (msg) => msg.createdTimestamp < statusMessage!.getMessage().createdTimestamp // (msg) => msg.createdTimestamp < statusMessage!.getMessage().createdTimestamp
//); //);
//TODO: Handle no messages to delete // Edit the message
if (!messages) { if (messages.size === 0) {
return await sendHybridInteractionMessageResponse(data, { let embed = EMBEDS.NOTHING_PURGED(data, locale);
embeds: [EMBEDS.SUCCESSFULLY_PURGED(data, locale)] if (data.isMessage()) {
}); return await statusMessage.getMessage().edit({ embeds: [embed] });
} else if (data.isApplicationCommand()) {
return await statusMessage.getMessageComponentInteraction().editReply({ embeds: [embed] });
}
} }
const deletedMessages = await (channel as BaseGuildTextChannel).bulkDelete(messages, true); while (totalMessageDeleted < totalMessagesToDelete) {
let messageSize = deletedMessages.size; amountToDelete =
totalMessagesToDelete - totalMessageDeleted > 100
? 100
: totalMessagesToDelete - totalMessageDeleted;
Logger.debug(
'[DiscordPurge]',
`Attempting to delete ${amountToDelete} messages. Total deleted: ${totalBulkDeletedMessages}/${totalMessagesToDelete}`
);
messages = await channel.messages.fetch({
limit: amountToDelete,
before: statusMessage.getMessage().id
});
if (messages.size === 0) {
let embed = EMBEDS.PURGING_DONE_EARLY(data, locale, totalBulkDeletedMessages, amountToDelete);
if (data.isMessage()) {
return await statusMessage.getMessage().edit({ embeds: [embed] });
} else if (data.isApplicationCommand()) {
return await statusMessage.getMessageComponentInteraction().editReply({ embeds: [embed] });
}
}
Logger.debug(
'[DiscordPurge]',
`Bulk deleting: ${messages.size}. Total deleted: ${totalBulkDeletedMessages}/${totalMessagesToDelete}`
);
const deletedMessages = await (channel as BaseGuildTextChannel).bulkDelete(messages, true);
totalMessageDeleted += deletedMessages.size;
totalBulkDeletedMessages += deletedMessages.size;
let embed = EMBEDS.PURGING_STAGE_1(data, locale, totalBulkDeletedMessages, totalMessagesToDelete);
if (data.isMessage()) {
await statusMessage.getMessage().edit({ embeds: [embed] });
} else if (data.isApplicationCommand()) {
await statusMessage.getMessageComponentInteraction().editReply({ embeds: [embed] });
}
// Unable to delete all the messages? Break the loop and start normal deletion
if (deletedMessages.size !== messages.size) break;
}
//let embed = EMBEDS.PURGING_DONE_EARLY(data, locale, totalBulkDeletedMessages, amountToDelete);
Logger.debug( Logger.debug(
'[DiscordPurge]', '[DiscordPurge]',
`Bulk deleted ${messageSize} messages. Total deleted: ${messageSize}/${totalMessagesToDelete}` `Bulk deleted ${totalBulkDeletedMessages} messages. Total deleted: ${totalBulkDeletedMessages}/${totalMessagesToDelete}`
); );
if (messageSize !== totalMessagesToDelete) { if (totalMessageDeleted !== totalMessagesToDelete) {
const remainingMessages = totalMessagesToDelete - messageSize; const remainingMessages = totalMessagesToDelete - totalMessageDeleted;
const fetchLimit = 100; const fetchLimit = 100;
const fetchIterations = Math.ceil(remainingMessages / fetchLimit); const fetchIterations = Math.ceil(remainingMessages / fetchLimit);
@@ -231,27 +332,62 @@ export default class Purge extends DiscordModule {
fetchedMessages = fetchedMessages.filter((msg) => msg.id !== statusMessage!.getMessage().id); fetchedMessages = fetchedMessages.filter((msg) => msg.id !== statusMessage!.getMessage().id);
let messageCounter = 0; // Counter for tracking every 5 messages
for (const msg of fetchedMessages.values()) { for (const msg of fetchedMessages.values()) {
await msg.delete(); await msg.delete();
messageSize += 1; totalMessageDeleted += 1;
Logger.debug('[DiscordPurge]', `Total deleted: ${messageSize}/${totalMessagesToDelete}`); messageCounter += 1;
if (messageCounter === 1 || messageCounter % 5 === 0) {
let embed = EMBEDS.PURGING_STAGE_2(
data,
locale,
totalBulkDeletedMessages,
totalMessageDeleted,
totalMessagesToDelete
);
if (data.isMessage()) {
await statusMessage.getMessage().edit({ embeds: [embed] });
} else if (data.isApplicationCommand()) {
await statusMessage.getMessageComponentInteraction().editReply({ embeds: [embed] });
}
Logger.debug(
'[DiscordPurge]',
`Total deleted: ${totalMessageDeleted}/${totalMessagesToDelete}`
);
}
// One second delay to prevent rate limit // One second delay to prevent rate limit
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
} }
} }
}
Logger.debug('[DiscordPurge]', `Total deleted: ${messageSize}/${totalMessagesToDelete}`); Logger.debug('[DiscordPurge]', `Total deleted: ${totalMessageDeleted}/${totalMessagesToDelete}`);
if (data.isMessage()) { let embed = EMBEDS.PURGING_DONE(
return await sendMessage(channel, data.getUser()!, { data,
embeds: [EMBEDS.SUCCESSFULLY_PURGED(data, locale)] locale,
}); totalBulkDeletedMessages,
} else if (data.isApplicationCommand()) { totalMessageDeleted - totalBulkDeletedMessages,
return await data amountToDelete
.getMessageComponentInteraction() );
.editReply({ embeds: [EMBEDS.SUCCESSFULLY_PURGED(data, locale)] }); if (data.isMessage()) {
return await statusMessage.getMessage().edit({ embeds: [embed] });
} else if (data.isApplicationCommand()) {
return await statusMessage.getMessageComponentInteraction().editReply({ embeds: [embed] });
}
} else {
Logger.debug('[DiscordPurge]', `Total deleted: ${totalMessageDeleted}/${totalMessagesToDelete}`);
let embed = EMBEDS.PURGING_DONE_EARLY(data, locale, totalBulkDeletedMessages, amountToDelete);
if (data.isMessage()) {
return await statusMessage.getMessage().edit({ embeds: [embed] });
} else if (data.isApplicationCommand()) {
return await statusMessage.getMessageComponentInteraction().editReply({ embeds: [embed] });
}
} }
} catch (err: any) { } catch (err: any) {
return await sendHybridInteractionMessageResponse(data, { return await sendHybridInteractionMessageResponse(data, {