Codex commited on
Commit ·
24acc72
1
Parent(s): d1c5d0f
Add user-gated bulk delete command
Browse files- src/commands.js +11 -0
- src/index.js +100 -0
- test/alerts.test.js +6 -0
src/commands.js
CHANGED
|
@@ -864,4 +864,15 @@ export const commands = [
|
|
| 864 |
new SlashCommandBuilder()
|
| 865 |
.setName('welcome')
|
| 866 |
.setDescription('Post the welcome embed and tag everyone.'),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
].map((command) => command.toJSON());
|
|
|
|
| 864 |
new SlashCommandBuilder()
|
| 865 |
.setName('welcome')
|
| 866 |
.setDescription('Post the welcome embed and tag everyone.'),
|
| 867 |
+
new SlashCommandBuilder()
|
| 868 |
+
.setName('bulkdeletemessages')
|
| 869 |
+
.setDescription('Bulk delete recent messages from the current channel.')
|
| 870 |
+
.addIntegerOption((option) =>
|
| 871 |
+
option
|
| 872 |
+
.setName('count')
|
| 873 |
+
.setDescription('How many of the most recent messages to inspect and bulk delete.')
|
| 874 |
+
.setRequired(true)
|
| 875 |
+
.setMinValue(1)
|
| 876 |
+
.setMaxValue(1000)
|
| 877 |
+
),
|
| 878 |
].map((command) => command.toJSON());
|
src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
| 4 |
ButtonStyle,
|
| 5 |
ChannelType,
|
| 6 |
Client,
|
|
|
|
| 7 |
Events,
|
| 8 |
GatewayIntentBits,
|
| 9 |
MessageFlags,
|
|
@@ -583,6 +584,11 @@ async function handleChatInput(interaction, store, config) {
|
|
| 583 |
|
| 584 |
if (commandName === 'welcome') {
|
| 585 |
await handleWelcome(interaction, config);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
}
|
| 587 |
}
|
| 588 |
|
|
@@ -1158,6 +1164,100 @@ async function handleAlerts(interaction) {
|
|
| 1158 |
});
|
| 1159 |
}
|
| 1160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1161 |
async function handleScanStatus(interaction, config) {
|
| 1162 |
const isAdmin = await memberHasRoleName(interaction, config.adminRoleName);
|
| 1163 |
if (!isAdmin) {
|
|
|
|
| 4 |
ButtonStyle,
|
| 5 |
ChannelType,
|
| 6 |
Client,
|
| 7 |
+
DiscordAPIError,
|
| 8 |
Events,
|
| 9 |
GatewayIntentBits,
|
| 10 |
MessageFlags,
|
|
|
|
| 584 |
|
| 585 |
if (commandName === 'welcome') {
|
| 586 |
await handleWelcome(interaction, config);
|
| 587 |
+
return;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
if (commandName === 'bulkdeletemessages') {
|
| 591 |
+
await handleBulkDeleteMessages(interaction);
|
| 592 |
}
|
| 593 |
}
|
| 594 |
|
|
|
|
| 1164 |
});
|
| 1165 |
}
|
| 1166 |
|
| 1167 |
+
async function handleBulkDeleteMessages(interaction) {
|
| 1168 |
+
if (interaction.user.username !== ALERTS_ALLOWED_USERNAME) {
|
| 1169 |
+
await interaction.reply({
|
| 1170 |
+
embeds: [
|
| 1171 |
+
buildErrorEmbed(
|
| 1172 |
+
'Not allowed',
|
| 1173 |
+
`Only **${ALERTS_ALLOWED_USERNAME}** can use this command.`
|
| 1174 |
+
),
|
| 1175 |
+
],
|
| 1176 |
+
flags: MessageFlags.Ephemeral,
|
| 1177 |
+
});
|
| 1178 |
+
return;
|
| 1179 |
+
}
|
| 1180 |
+
|
| 1181 |
+
if (!interaction.inGuild() || !interaction.channel || interaction.channel.type === ChannelType.DM || typeof interaction.channel.bulkDelete !== 'function') {
|
| 1182 |
+
await interaction.reply({
|
| 1183 |
+
embeds: [
|
| 1184 |
+
buildErrorEmbed(
|
| 1185 |
+
'Channel unavailable',
|
| 1186 |
+
'This command can only bulk delete messages in a server text channel.'
|
| 1187 |
+
),
|
| 1188 |
+
],
|
| 1189 |
+
flags: MessageFlags.Ephemeral,
|
| 1190 |
+
});
|
| 1191 |
+
return;
|
| 1192 |
+
}
|
| 1193 |
+
|
| 1194 |
+
const requestedCount = interaction.options.getInteger('count', true);
|
| 1195 |
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
| 1196 |
+
|
| 1197 |
+
const now = Date.now();
|
| 1198 |
+
const bulkDeleteCutoffMs = 14 * 24 * 60 * 60 * 1000;
|
| 1199 |
+
let remainingToInspect = requestedCount;
|
| 1200 |
+
let beforeMessageId;
|
| 1201 |
+
let deletedCount = 0;
|
| 1202 |
+
let skippedCount = 0;
|
| 1203 |
+
|
| 1204 |
+
try {
|
| 1205 |
+
while (remainingToInspect > 0) {
|
| 1206 |
+
const fetchLimit = Math.min(100, remainingToInspect);
|
| 1207 |
+
const batch = await interaction.channel.messages.fetch({
|
| 1208 |
+
limit: fetchLimit,
|
| 1209 |
+
...(beforeMessageId ? { before: beforeMessageId } : {}),
|
| 1210 |
+
});
|
| 1211 |
+
|
| 1212 |
+
if (batch.size === 0) {
|
| 1213 |
+
break;
|
| 1214 |
+
}
|
| 1215 |
+
|
| 1216 |
+
remainingToInspect -= batch.size;
|
| 1217 |
+
beforeMessageId = batch.last()?.id;
|
| 1218 |
+
|
| 1219 |
+
const deletableMessages = batch.filter((message) =>
|
| 1220 |
+
!message.pinned
|
| 1221 |
+
&& !message.system
|
| 1222 |
+
&& (now - message.createdTimestamp) < bulkDeleteCutoffMs
|
| 1223 |
+
);
|
| 1224 |
+
|
| 1225 |
+
skippedCount += batch.size - deletableMessages.size;
|
| 1226 |
+
|
| 1227 |
+
if (deletableMessages.size === 0) {
|
| 1228 |
+
continue;
|
| 1229 |
+
}
|
| 1230 |
+
|
| 1231 |
+
const deleted = await interaction.channel.bulkDelete(deletableMessages, true);
|
| 1232 |
+
deletedCount += deleted.size;
|
| 1233 |
+
}
|
| 1234 |
+
|
| 1235 |
+
await finalizeDeferredInteraction(interaction, {
|
| 1236 |
+
embeds: [
|
| 1237 |
+
buildErrorEmbed(
|
| 1238 |
+
'Bulk delete complete',
|
| 1239 |
+
`Deleted **${deletedCount}** recent message${deletedCount === 1 ? '' : 's'} from this channel.${skippedCount > 0 ? ` Skipped **${skippedCount}** pinned, system, or older-than-14-days message${skippedCount === 1 ? '' : 's'}.` : ''}`
|
| 1240 |
+
).setColor(BRAND_COLORS.primary),
|
| 1241 |
+
],
|
| 1242 |
+
flags: MessageFlags.Ephemeral,
|
| 1243 |
+
});
|
| 1244 |
+
} catch (error) {
|
| 1245 |
+
if (error instanceof DiscordAPIError && error.code === 50013) {
|
| 1246 |
+
await finalizeDeferredInteraction(interaction, {
|
| 1247 |
+
embeds: [
|
| 1248 |
+
buildErrorEmbed(
|
| 1249 |
+
'Missing permissions',
|
| 1250 |
+
'The bot needs permission to manage messages in this channel before it can bulk delete them.'
|
| 1251 |
+
),
|
| 1252 |
+
],
|
| 1253 |
+
flags: MessageFlags.Ephemeral,
|
| 1254 |
+
});
|
| 1255 |
+
return;
|
| 1256 |
+
}
|
| 1257 |
+
throw error;
|
| 1258 |
+
}
|
| 1259 |
+
}
|
| 1260 |
+
|
| 1261 |
async function handleScanStatus(interaction, config) {
|
| 1262 |
const isAdmin = await memberHasRoleName(interaction, config.adminRoleName);
|
| 1263 |
if (!isAdmin) {
|
test/alerts.test.js
CHANGED
|
@@ -17,6 +17,12 @@ test('registers the alerts slash command', () => {
|
|
| 17 |
assert.match(alertsCommand.description, /alert-role embed/i);
|
| 18 |
});
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
test('lists alerts in the public commands embed', () => {
|
| 21 |
const embed = buildCommandsEmbed().toJSON();
|
| 22 |
const alertsField = embed.fields.find((field) => field.name === '/alerts');
|
|
|
|
| 17 |
assert.match(alertsCommand.description, /alert-role embed/i);
|
| 18 |
});
|
| 19 |
|
| 20 |
+
test('registers the bulk delete slash command', () => {
|
| 21 |
+
const bulkDeleteCommand = commands.find((command) => command.name === 'bulkdeletemessages');
|
| 22 |
+
assert.ok(bulkDeleteCommand);
|
| 23 |
+
assert.match(bulkDeleteCommand.description, /bulk delete recent messages/i);
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
test('lists alerts in the public commands embed', () => {
|
| 27 |
const embed = buildCommandsEmbed().toJSON();
|
| 28 |
const alertsField = embed.fields.find((field) => field.name === '/alerts');
|