Codex commited on
Commit
24acc72
·
1 Parent(s): d1c5d0f

Add user-gated bulk delete command

Browse files
Files changed (3) hide show
  1. src/commands.js +11 -0
  2. src/index.js +100 -0
  3. 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');