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_Channel String?
Purge_Limit Int @default(200)
Purge_Limit Int @default(300)
}
model User {
+213 -77
View File
@@ -4,7 +4,8 @@ import {
PermissionsBitField,
GuildBasedChannel,
BaseGuildTextChannel,
BaseInteraction
BaseInteraction,
GuildTextBasedChannel
} from 'discord.js';
import { I18n } from 'i18n';
@@ -20,6 +21,11 @@ import Locale from '../services/Locale';
import Logger from '../libs/Logger';
import Prisma from '../providers/Prisma';
import Environment from '../providers/Environment';
import {
checkBotPermissionsInChannel,
checkMemberPermissionsInChannel,
checkMemberPermissionsInGuild
} from '../utils/DiscordPermission';
const EMBEDS = {
PURGE_INFO: (data: HybridInteractionMessage, locale: I18n) => {
@@ -35,19 +41,65 @@ const EMBEDS = {
user: data.getUser()
});
},
NO_PERMISSION: (data: HybridInteractionMessage, locale: I18n) => {
return makeErrorEmbed({
title: locale.__('common.no_permissions'),
description: locale.__('common.no_permissions_description', {
PERMISSIONS: 'VIEW_CHANNEL, MANAGE_MESSAGES'
}),
PURGING_STAGE_1: (data: HybridInteractionMessage, locale: I18n, current: number, total: number) => {
return makeInfoEmbed({
title: locale.__('purge.title'),
description:
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()
});
},
PURGING: (data: HybridInteractionMessage, locale: I18n) => {
PURGING_STAGE_2: (
data: HybridInteractionMessage,
locale: I18n,
current: number,
current_normal: number,
total: number
) => {
return makeInfoEmbed({
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()
});
},
@@ -60,21 +112,21 @@ const EMBEDS = {
},
PURGE_NAN_ERROR: (data: HybridInteractionMessage, locale: I18n) => {
return makeErrorEmbed({
title: locale.__('purge.error'),
description: locale.__('purge.error_nan'),
title: locale.__('purge.nan_error_title'),
description: locale.__('purge.nan_error_description'),
user: data.getUser()
});
},
PURGE_OVER_LIMIT: (data: HybridInteractionMessage, locale: I18n) => {
PURGE_OVER_LIMIT: (data: HybridInteractionMessage, locale: I18n, limit: number) => {
return makeErrorEmbed({
title: locale.__('purge.error'),
description: locale.__('purge.error_over_limit'),
title: locale.__('common.member_no_permissions'),
description: locale.__('purge.error_over_limit', { MAX: limit.toString() }),
user: data.getUser()
});
},
SUCCESSFULLY_PURGED: (data: HybridInteractionMessage, locale: I18n) => {
NOTHING_PURGED: (data: HybridInteractionMessage, locale: I18n) => {
return makeSuccessEmbed({
title: locale.__('purge.success'),
title: locale.__('purge.nothing_purged'),
user: data.getUser()
});
}
@@ -94,34 +146,24 @@ export default class Purge extends DiscordModule {
}
async run(data: HybridInteractionMessage, args: any) {
// Disable this unfinished feature in production
if (Environment.get().NODE_ENV !== 'production') return;
const guild = data.getGuild();
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);
let query;
const channel = data.getChannel();
if (!channel) return;
if (data.isMessage()) {
const message = data.getMessage();
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)
return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.PURGE_INFO(data, locale)]
@@ -130,20 +172,6 @@ export default class Purge extends DiscordModule {
query = args.join(' ');
} else if (data.isApplicationCommand()) {
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();
}
@@ -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 } });
if (!PrismaGuild) return;
@@ -161,13 +211,15 @@ export default class Purge extends DiscordModule {
try {
let totalMessagesToDelete = parseInt(query);
// if (totalMessagesToDelete > guildLimit) {
// return await sendHybridInteractionMessageResponse(data, {
// embeds: [EMBEDS.PURGE_OVER_LIMIT(data, locale)]
// });
// }
if (totalMessagesToDelete > guildLimit) {
return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.PURGE_OVER_LIMIT(data, locale, guildLimit)]
});
}
let totalMessageDeleted = 0;
let totalBulkDeletedMessages = 0;
let amountToDelete = totalMessagesToDelete > 100 ? 100 : totalMessagesToDelete;
let messages;
let statusMessage;
if (data.isMessage()) {
@@ -178,20 +230,22 @@ export default class Purge extends DiscordModule {
.catch(() => {});
statusMessage = await sendMessage(channel, data.getUser()!, {
embeds: [EMBEDS.PURGING(data, locale)]
embeds: [EMBEDS.PURGING_STAGE_1(data, locale, totalBulkDeletedMessages, totalMessagesToDelete)]
});
} else if (data.isApplicationCommand()) {
statusMessage = await data
.getMessageComponentInteraction()
.reply({ embeds: [EMBEDS.PURGING(data, locale)], fetchReply: true });
statusMessage = await data.getMessageComponentInteraction().reply({
embeds: [EMBEDS.PURGING_STAGE_1(data, locale, totalBulkDeletedMessages, totalMessagesToDelete)],
fetchReply: true
});
statusMessage = statusMessage as unknown as BaseInteraction;
}
statusMessage = new HybridInteractionMessage(statusMessage!);
let amountToDelete = totalMessagesToDelete > 100 ? 100 : totalMessagesToDelete;
messages = await channel.messages.fetch({
limit: amountToDelete,
before: statusMessage!.getMessage().id
before: statusMessage.getMessage().id
});
// Filter out statusMessage
@@ -202,23 +256,70 @@ export default class Purge extends DiscordModule {
// (msg) => msg.createdTimestamp < statusMessage!.getMessage().createdTimestamp
//);
//TODO: Handle no messages to delete
if (!messages) {
return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.SUCCESSFULLY_PURGED(data, locale)]
});
// Edit the message
if (messages.size === 0) {
let embed = EMBEDS.NOTHING_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);
let messageSize = deletedMessages.size;
while (totalMessageDeleted < totalMessagesToDelete) {
amountToDelete =
totalMessagesToDelete - totalMessageDeleted > 100
? 100
: totalMessagesToDelete - totalMessageDeleted;
Logger.debug(
'[DiscordPurge]',
`Bulk deleted ${messageSize} messages. Total deleted: ${messageSize}/${totalMessagesToDelete}`
`Attempting to delete ${amountToDelete} messages. Total deleted: ${totalBulkDeletedMessages}/${totalMessagesToDelete}`
);
if (messageSize !== totalMessagesToDelete) {
const remainingMessages = totalMessagesToDelete - messageSize;
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(
'[DiscordPurge]',
`Bulk deleted ${totalBulkDeletedMessages} messages. Total deleted: ${totalBulkDeletedMessages}/${totalMessagesToDelete}`
);
if (totalMessageDeleted !== totalMessagesToDelete) {
const remainingMessages = totalMessagesToDelete - totalMessageDeleted;
const fetchLimit = 100;
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);
let messageCounter = 0; // Counter for tracking every 5 messages
for (const msg of fetchedMessages.values()) {
await msg.delete();
messageSize += 1;
Logger.debug('[DiscordPurge]', `Total deleted: ${messageSize}/${totalMessagesToDelete}`);
totalMessageDeleted += 1;
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
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
}
Logger.debug('[DiscordPurge]', `Total deleted: ${messageSize}/${totalMessagesToDelete}`);
Logger.debug('[DiscordPurge]', `Total deleted: ${totalMessageDeleted}/${totalMessagesToDelete}`);
let embed = EMBEDS.PURGING_DONE(
data,
locale,
totalBulkDeletedMessages,
totalMessageDeleted - totalBulkDeletedMessages,
amountToDelete
);
if (data.isMessage()) {
return await sendMessage(channel, data.getUser()!, {
embeds: [EMBEDS.SUCCESSFULLY_PURGED(data, locale)]
});
return await statusMessage.getMessage().edit({ embeds: [embed] });
} else if (data.isApplicationCommand()) {
return await data
.getMessageComponentInteraction()
.editReply({ embeds: [EMBEDS.SUCCESSFULLY_PURGED(data, locale)] });
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) {
return await sendHybridInteractionMessageResponse(data, {