adb-plugin-moderation 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # adb-plugin-moderation
2
+
3
+ Core moderation commands for [Advanced Discord Bot](https://github.com/dead/Advanced-Discord-Bot).
4
+
5
+ ## What it does
6
+
7
+ - **Actions**: ban, unban, kick, timeout, untimeout, warn
8
+ - **Records**: case log, per-member warning history, moderator notes
9
+ - **Queries**: case lookup, user history, modstats (monthly breakdown)
10
+ - **Utilities**: purge messages, slowmode, lock/unlock channels
11
+ - **Tickets**: setup category/role, open/close/add/remove users
12
+
13
+ All actions create a numbered case and optionally post an embed to a log channel. Warnings can trigger automatic escalation (timeout → kick → ban) at configurable thresholds.
14
+
15
+ ## Requirements
16
+
17
+ - Node.js 18+
18
+ - Advanced Discord Bot with plugin loader
19
+ - MongoDB (via ADB's mongoose connection)
20
+ - Discord bot with these permissions:
21
+ - Ban Members
22
+ - Kick Members
23
+ - Moderate Members (timeout)
24
+ - Manage Messages (warn, purge, note)
25
+ - Manage Channels (slowmode, lock, ticket channels)
26
+ - Manage Guild (clearwarnings, modstats, ticket setup)
27
+
28
+ ## Installation
29
+
30
+ Inside your bot directory:
31
+
32
+ ```bash
33
+ npm install adb-plugin-moderation
34
+ ```
35
+
36
+ Then add the plugin to your ADB config:
37
+
38
+ ```json
39
+ {
40
+ "plugins": ["adb-plugin-moderation"]
41
+ }
42
+ ```
43
+
44
+ Restart the bot. The plugin registers slash commands on load — deploy them to Discord:
45
+
46
+ ```bash
47
+ node deploy-commands.js
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ Run `/ticket setup` in your server to configure tickets. All other settings go in the ADB plugin config UI or directly via the database.
53
+
54
+ | Key | Default | Description |
55
+ |---|---|---|
56
+ | `log_channel_id` | null | Channel to post case embeds |
57
+ | `ticket_category_id` | null | Category for ticket channels |
58
+ | `ticket_support_role_id` | null | Role pinged when a ticket opens |
59
+ | `ticket_log_channel_id` | null | Channel for ticket transcripts on close |
60
+ | `dm_on_action` | true | DM users when actioned |
61
+ | `warn_thresholds` | see below | Auto-escalation on warning count |
62
+
63
+ ### Default warn thresholds
64
+
65
+ ```json
66
+ {
67
+ "3": { "action": "timeout", "duration": "1h" },
68
+ "5": { "action": "timeout", "duration": "24h" },
69
+ "7": { "action": "kick" },
70
+ "10": { "action": "ban" }
71
+ }
72
+ ```
73
+
74
+ ## Commands
75
+
76
+ | Command | Permission | Description |
77
+ |---|---|---|
78
+ | `/ban` | Ban Members | Ban a user |
79
+ | `/unban` | Ban Members | Unban by user ID |
80
+ | `/kick` | Kick Members | Kick a member |
81
+ | `/timeout` | Moderate Members | Timeout (1m–28d) |
82
+ | `/untimeout` | Moderate Members | Remove timeout |
83
+ | `/warn` | Manage Messages | Issue a warning |
84
+ | `/warnings` | Manage Messages | View warnings for a user |
85
+ | `/clearwarnings` | Manage Guild | Clear all warnings |
86
+ | `/note` | Manage Messages | Add a private mod note |
87
+ | `/purge` | Manage Messages | Bulk-delete messages |
88
+ | `/slowmode` | Manage Channels | Set channel slowmode |
89
+ | `/lock` | Manage Channels | Prevent @everyone from sending |
90
+ | `/unlock` | Manage Channels | Restore @everyone send permission |
91
+ | `/case` | Manage Messages | Look up a case by number |
92
+ | `/history` | Manage Messages | View a user's case history |
93
+ | `/modstats` | Manage Guild | View a moderator's monthly stats |
94
+ | `/ticket setup` | Manage Guild | Configure the ticket system |
95
+ | `/ticket open` | Everyone | Open a support ticket |
96
+ | `/ticket close` | Manage Channels | Close and archive a ticket |
97
+ | `/ticket add` | Manage Channels | Add a user to a ticket |
98
+ | `/ticket remove` | Manage Channels | Remove a user from a ticket |
99
+
100
+ ## Testing
101
+
102
+ ```bash
103
+ npm test
104
+ ```
105
+
106
+ Runs the local harness — no Discord or MongoDB connection required.
107
+
108
+ ## License
109
+
110
+ MIT
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+
3
+ const { EmbedBuilder, PermissionFlagsBits } = require("discord.js");
4
+ const { requirePerms } = require("../lib/permissions");
5
+ const { dmActionUser } = require("../lib/dmUser");
6
+ const { createCase, postCaseLog } = require("../lib/logCase");
7
+
8
+ module.exports = {
9
+ data: {
10
+ name: "ban",
11
+ description: "Ban a member from the server",
12
+ options: [
13
+ { type: 6, name: "user", description: "The user to ban", required: true },
14
+ { type: 3, name: "reason", description: "Reason for the ban", required: false },
15
+ {
16
+ type: 4,
17
+ name: "days",
18
+ description: "Days of messages to delete (0-7)",
19
+ required: false,
20
+ min_value: 0,
21
+ max_value: 7,
22
+ },
23
+ ],
24
+ },
25
+
26
+ async execute(interaction, ctx) {
27
+ if (!requirePerms(interaction, PermissionFlagsBits.BanMembers)) return;
28
+
29
+ const targetUser = interaction.options.getUser("user");
30
+ const reason = interaction.options.getString("reason") || "No reason provided";
31
+ const days = interaction.options.getInteger("days") ?? 0;
32
+ const guildId = interaction.guild.id;
33
+
34
+ const CaseModel = ctx.models.Case;
35
+ const configData = (await ctx.db.getPluginConfig(guildId, "adb-plugin-moderation"))?.data || {};
36
+
37
+ let member;
38
+ try {
39
+ member = await interaction.guild.members.fetch(targetUser.id);
40
+ } catch {
41
+ // User not in guild — still attempt ban
42
+ }
43
+
44
+ if (member && !member.bannable) {
45
+ const embed = new EmbedBuilder()
46
+ .setColor(0xe74c3c)
47
+ .setDescription("I cannot ban that member (insufficient role hierarchy).");
48
+ return interaction.reply({ embeds: [embed], ephemeral: true });
49
+ }
50
+
51
+ if (configData.dm_on_action !== false) {
52
+ await dmActionUser(ctx.client, targetUser.id, {
53
+ action: "banned",
54
+ guildName: interaction.guild.name,
55
+ reason,
56
+ });
57
+ }
58
+
59
+ try {
60
+ await interaction.guild.members.ban(targetUser.id, {
61
+ reason,
62
+ deleteMessageSeconds: days * 86400,
63
+ });
64
+ } catch (err) {
65
+ const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to ban: ${err.message}`);
66
+ return interaction.reply({ embeds: [embed], ephemeral: true });
67
+ }
68
+
69
+ const caseDoc = await createCase(CaseModel, {
70
+ guildId,
71
+ type: "ban",
72
+ targetId: targetUser.id,
73
+ moderatorId: interaction.user.id,
74
+ reason,
75
+ });
76
+
77
+ await postCaseLog(ctx, configData, caseDoc, targetUser, interaction.user);
78
+
79
+ const embed = new EmbedBuilder()
80
+ .setColor(0xe74c3c)
81
+ .setTitle(`Banned — Case #${caseDoc.caseNumber}`)
82
+ .addFields(
83
+ { name: "User", value: `${targetUser.tag} (${targetUser.id})`, inline: true },
84
+ { name: "Reason", value: reason }
85
+ )
86
+ .setTimestamp();
87
+
88
+ await interaction.reply({ embeds: [embed], ephemeral: true });
89
+ },
90
+ };
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+
3
+ const { EmbedBuilder, PermissionFlagsBits } = require("discord.js");
4
+ const { requirePerms } = require("../lib/permissions");
5
+
6
+ const TYPE_COLORS = {
7
+ ban: 0xe74c3c,
8
+ unban: 0x2ecc71,
9
+ kick: 0xe67e22,
10
+ timeout: 0xf39c12,
11
+ untimeout: 0x3498db,
12
+ warn: 0xf1c40f,
13
+ note: 0x95a5a6,
14
+ };
15
+
16
+ module.exports = {
17
+ data: {
18
+ name: "case",
19
+ description: "Look up a moderation case by number",
20
+ options: [
21
+ { type: 4, name: "id", description: "Case number", required: true, min_value: 1 },
22
+ ],
23
+ },
24
+
25
+ async execute(interaction, ctx) {
26
+ if (!requirePerms(interaction, PermissionFlagsBits.ManageMessages)) return;
27
+
28
+ const caseNumber = interaction.options.getInteger("id");
29
+ const guildId = interaction.guild.id;
30
+ const CaseModel = ctx.models.Case;
31
+
32
+ const caseDoc = await CaseModel.findOne({ guildId, caseNumber }).lean();
33
+
34
+ if (!caseDoc) {
35
+ const embed = new EmbedBuilder()
36
+ .setColor(0xe74c3c)
37
+ .setDescription(`Case #${caseNumber} not found.`);
38
+ return interaction.reply({ embeds: [embed], ephemeral: true });
39
+ }
40
+
41
+ let targetUser = null;
42
+ let moderator = null;
43
+ try { targetUser = await ctx.client.users.fetch(caseDoc.targetUserId); } catch {}
44
+ try { moderator = await ctx.client.users.fetch(caseDoc.moderatorId); } catch {}
45
+
46
+ const color = TYPE_COLORS[caseDoc.type] || 0x99aab5;
47
+ const embed = new EmbedBuilder()
48
+ .setColor(color)
49
+ .setTitle(`Case #${caseDoc.caseNumber} — ${caseDoc.type.toUpperCase()}`)
50
+ .addFields(
51
+ { name: "Target", value: targetUser ? `${targetUser.tag} (${caseDoc.targetUserId})` : caseDoc.targetUserId, inline: true },
52
+ { name: "Moderator", value: moderator ? `${moderator.tag} (${caseDoc.moderatorId})` : caseDoc.moderatorId, inline: true },
53
+ { name: "Reason", value: caseDoc.reason || "No reason provided" }
54
+ )
55
+ .setTimestamp(caseDoc.createdAt);
56
+
57
+ if (caseDoc.duration) {
58
+ embed.addFields({ name: "Duration", value: caseDoc.duration, inline: true });
59
+ }
60
+
61
+ await interaction.reply({ embeds: [embed], ephemeral: true });
62
+ },
63
+ };
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+
3
+ const { EmbedBuilder, PermissionFlagsBits } = require("discord.js");
4
+ const { requirePerms } = require("../lib/permissions");
5
+
6
+ module.exports = {
7
+ data: {
8
+ name: "clearwarnings",
9
+ description: "Clear all warnings for a member",
10
+ options: [
11
+ { type: 6, name: "user", description: "The member to clear warnings for", required: true },
12
+ ],
13
+ },
14
+
15
+ async execute(interaction, ctx) {
16
+ if (!requirePerms(interaction, PermissionFlagsBits.ManageGuild)) return;
17
+
18
+ const targetUser = interaction.options.getUser("user");
19
+ const guildId = interaction.guild.id;
20
+ const CaseModel = ctx.models.Case;
21
+
22
+ const result = await CaseModel.deleteMany({ guildId, targetUserId: targetUser.id, type: "warn" });
23
+ await ctx.db.updateUserProfile(targetUser.id, guildId, { warnings: 0 });
24
+
25
+ const embed = new EmbedBuilder()
26
+ .setColor(0x2ecc71)
27
+ .setTitle("Warnings Cleared")
28
+ .setDescription(
29
+ `Cleared **${result.deletedCount}** warning(s) for **${targetUser.tag}**.`
30
+ )
31
+ .setTimestamp();
32
+
33
+ await interaction.reply({ embeds: [embed], ephemeral: true });
34
+ },
35
+ };
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+
3
+ const { EmbedBuilder, PermissionFlagsBits } = require("discord.js");
4
+ const { requirePerms } = require("../lib/permissions");
5
+
6
+ module.exports = {
7
+ data: {
8
+ name: "history",
9
+ description: "View moderation history for a member",
10
+ options: [
11
+ { type: 6, name: "user", description: "The member to look up", required: true },
12
+ ],
13
+ },
14
+
15
+ async execute(interaction, ctx) {
16
+ if (!requirePerms(interaction, PermissionFlagsBits.ManageMessages)) return;
17
+
18
+ const targetUser = interaction.options.getUser("user");
19
+ const guildId = interaction.guild.id;
20
+ const CaseModel = ctx.models.Case;
21
+
22
+ const cases = await CaseModel.find({ guildId, targetUserId: targetUser.id })
23
+ .sort({ createdAt: -1 })
24
+ .limit(20)
25
+ .lean();
26
+
27
+ const embed = new EmbedBuilder()
28
+ .setColor(0x99aab5)
29
+ .setTitle(`History for ${targetUser.tag}`)
30
+ .setThumbnail(targetUser.displayAvatarURL())
31
+ .setDescription(
32
+ cases.length === 0
33
+ ? "No moderation history found."
34
+ : cases
35
+ .map(
36
+ (c) =>
37
+ `**[#${c.caseNumber}] ${c.type.toUpperCase()}** — <t:${Math.floor(new Date(c.createdAt).getTime() / 1000)}:R>\n${c.reason}`
38
+ )
39
+ .join("\n\n")
40
+ )
41
+ .setFooter({ text: `Showing up to 20 most recent cases` })
42
+ .setTimestamp();
43
+
44
+ await interaction.reply({ embeds: [embed], ephemeral: true });
45
+ },
46
+ };
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+
3
+ const { EmbedBuilder, PermissionFlagsBits } = require("discord.js");
4
+ const { requirePerms } = require("../lib/permissions");
5
+ const { dmActionUser } = require("../lib/dmUser");
6
+ const { createCase, postCaseLog } = require("../lib/logCase");
7
+
8
+ module.exports = {
9
+ data: {
10
+ name: "kick",
11
+ description: "Kick a member from the server",
12
+ options: [
13
+ { type: 6, name: "user", description: "The member to kick", required: true },
14
+ { type: 3, name: "reason", description: "Reason for the kick", required: false },
15
+ ],
16
+ },
17
+
18
+ async execute(interaction, ctx) {
19
+ if (!requirePerms(interaction, PermissionFlagsBits.KickMembers)) return;
20
+
21
+ const targetUser = interaction.options.getUser("user");
22
+ const reason = interaction.options.getString("reason") || "No reason provided";
23
+ const guildId = interaction.guild.id;
24
+
25
+ const CaseModel = ctx.models.Case;
26
+ const configData = (await ctx.db.getPluginConfig(guildId, "adb-plugin-moderation"))?.data || {};
27
+
28
+ let member;
29
+ try {
30
+ member = await interaction.guild.members.fetch(targetUser.id);
31
+ } catch {
32
+ const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription("That user is not in this server.");
33
+ return interaction.reply({ embeds: [embed], ephemeral: true });
34
+ }
35
+
36
+ if (!member.kickable) {
37
+ const embed = new EmbedBuilder()
38
+ .setColor(0xe74c3c)
39
+ .setDescription("I cannot kick that member (insufficient role hierarchy).");
40
+ return interaction.reply({ embeds: [embed], ephemeral: true });
41
+ }
42
+
43
+ if (configData.dm_on_action !== false) {
44
+ await dmActionUser(ctx.client, targetUser.id, {
45
+ action: "kicked",
46
+ guildName: interaction.guild.name,
47
+ reason,
48
+ });
49
+ }
50
+
51
+ try {
52
+ await member.kick(reason);
53
+ } catch (err) {
54
+ const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to kick: ${err.message}`);
55
+ return interaction.reply({ embeds: [embed], ephemeral: true });
56
+ }
57
+
58
+ const caseDoc = await createCase(CaseModel, {
59
+ guildId,
60
+ type: "kick",
61
+ targetId: targetUser.id,
62
+ moderatorId: interaction.user.id,
63
+ reason,
64
+ });
65
+
66
+ await postCaseLog(ctx, configData, caseDoc, targetUser, interaction.user);
67
+
68
+ const embed = new EmbedBuilder()
69
+ .setColor(0xe67e22)
70
+ .setTitle(`Kicked — Case #${caseDoc.caseNumber}`)
71
+ .addFields(
72
+ { name: "User", value: `${targetUser.tag} (${targetUser.id})`, inline: true },
73
+ { name: "Reason", value: reason }
74
+ )
75
+ .setTimestamp();
76
+
77
+ await interaction.reply({ embeds: [embed], ephemeral: true });
78
+ },
79
+ };
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+
3
+ const { EmbedBuilder, PermissionFlagsBits } = require("discord.js");
4
+ const { requirePerms } = require("../lib/permissions");
5
+
6
+ module.exports = {
7
+ data: {
8
+ name: "lock",
9
+ description: "Lock a channel (prevent @everyone from sending messages)",
10
+ options: [
11
+ { type: 7, name: "channel", description: "Channel to lock (defaults to current)", required: false },
12
+ ],
13
+ },
14
+
15
+ async execute(interaction, ctx) {
16
+ if (!requirePerms(interaction, PermissionFlagsBits.ManageChannels)) return;
17
+
18
+ const target = interaction.options.getChannel("channel") || interaction.channel;
19
+
20
+ try {
21
+ await target.permissionOverwrites.edit(interaction.guild.roles.everyone, {
22
+ SendMessages: false,
23
+ });
24
+ } catch (err) {
25
+ const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to lock channel: ${err.message}`);
26
+ return interaction.reply({ embeds: [embed], ephemeral: true });
27
+ }
28
+
29
+ const embed = new EmbedBuilder()
30
+ .setColor(0xe74c3c)
31
+ .setDescription(`${target} has been **locked**. Members can no longer send messages.`)
32
+ .setTimestamp();
33
+
34
+ await interaction.reply({ embeds: [embed], ephemeral: true });
35
+ },
36
+ };
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ const { EmbedBuilder, PermissionFlagsBits } = require("discord.js");
4
+ const { requirePerms } = require("../lib/permissions");
5
+
6
+ module.exports = {
7
+ data: {
8
+ name: "modstats",
9
+ description: "View moderation statistics for a moderator",
10
+ options: [
11
+ { type: 6, name: "mod", description: "Moderator to check (defaults to yourself)", required: false },
12
+ ],
13
+ },
14
+
15
+ async execute(interaction, ctx) {
16
+ if (!requirePerms(interaction, PermissionFlagsBits.ManageGuild)) return;
17
+
18
+ const modUser = interaction.options.getUser("mod") || interaction.user;
19
+ const guildId = interaction.guild.id;
20
+ const CaseModel = ctx.models.Case;
21
+
22
+ const startOfMonth = new Date();
23
+ startOfMonth.setDate(1);
24
+ startOfMonth.setHours(0, 0, 0, 0);
25
+
26
+ const types = ["ban", "unban", "kick", "timeout", "untimeout", "warn", "note"];
27
+
28
+ const counts = await Promise.all(
29
+ types.map((type) =>
30
+ CaseModel.countDocuments({
31
+ guildId,
32
+ moderatorId: modUser.id,
33
+ type,
34
+ createdAt: { $gte: startOfMonth },
35
+ })
36
+ )
37
+ );
38
+
39
+ const total = counts.reduce((a, b) => a + b, 0);
40
+
41
+ const embed = new EmbedBuilder()
42
+ .setColor(0x3498db)
43
+ .setTitle(`Mod Stats — ${modUser.tag}`)
44
+ .setThumbnail(modUser.displayAvatarURL())
45
+ .setDescription(`Actions taken since **<t:${Math.floor(startOfMonth.getTime() / 1000)}:D>**`)
46
+ .addFields(
47
+ types.map((type, i) => ({ name: type.toUpperCase(), value: String(counts[i]), inline: true }))
48
+ )
49
+ .addFields({ name: "Total (this month)", value: String(total), inline: false })
50
+ .setTimestamp();
51
+
52
+ await interaction.reply({ embeds: [embed], ephemeral: true });
53
+ },
54
+ };
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ const { EmbedBuilder, PermissionFlagsBits } = require("discord.js");
4
+ const { requirePerms } = require("../lib/permissions");
5
+ const { createCase, postCaseLog } = require("../lib/logCase");
6
+
7
+ module.exports = {
8
+ data: {
9
+ name: "note",
10
+ description: "Add a moderator note to a member",
11
+ options: [
12
+ { type: 6, name: "user", description: "The member to add a note to", required: true },
13
+ { type: 3, name: "text", description: "The note content", required: true },
14
+ ],
15
+ },
16
+
17
+ async execute(interaction, ctx) {
18
+ if (!requirePerms(interaction, PermissionFlagsBits.ManageMessages)) return;
19
+
20
+ const targetUser = interaction.options.getUser("user");
21
+ const text = interaction.options.getString("text");
22
+ const guildId = interaction.guild.id;
23
+
24
+ const CaseModel = ctx.models.Case;
25
+ const NoteModel = ctx.models.Note;
26
+ const configData = (await ctx.db.getPluginConfig(guildId, "adb-plugin-moderation"))?.data || {};
27
+
28
+ // Save note document
29
+ const noteDoc = new NoteModel({
30
+ guildId,
31
+ userId: targetUser.id,
32
+ moderatorId: interaction.user.id,
33
+ note: text,
34
+ });
35
+ await noteDoc.save();
36
+
37
+ const caseDoc = await createCase(CaseModel, {
38
+ guildId,
39
+ type: "note",
40
+ targetId: targetUser.id,
41
+ moderatorId: interaction.user.id,
42
+ reason: text,
43
+ });
44
+
45
+ await postCaseLog(ctx, configData, caseDoc, targetUser, interaction.user);
46
+
47
+ const embed = new EmbedBuilder()
48
+ .setColor(0x95a5a6)
49
+ .setTitle(`Note Added — Case #${caseDoc.caseNumber}`)
50
+ .addFields(
51
+ { name: "User", value: `${targetUser.tag} (${targetUser.id})`, inline: true },
52
+ { name: "Note", value: text }
53
+ )
54
+ .setTimestamp();
55
+
56
+ await interaction.reply({ embeds: [embed], ephemeral: true });
57
+ },
58
+ };
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+
3
+ const { EmbedBuilder, PermissionFlagsBits } = require("discord.js");
4
+ const { requirePerms } = require("../lib/permissions");
5
+
6
+ module.exports = {
7
+ data: {
8
+ name: "purge",
9
+ description: "Bulk-delete messages from this channel",
10
+ options: [
11
+ {
12
+ type: 4,
13
+ name: "amount",
14
+ description: "Number of messages to delete (1-100)",
15
+ required: true,
16
+ min_value: 1,
17
+ max_value: 100,
18
+ },
19
+ { type: 6, name: "user", description: "Only delete messages from this user", required: false },
20
+ ],
21
+ },
22
+
23
+ async execute(interaction, ctx) {
24
+ if (!requirePerms(interaction, PermissionFlagsBits.ManageMessages)) return;
25
+
26
+ const amount = interaction.options.getInteger("amount");
27
+ const filterUser = interaction.options.getUser("user");
28
+ const channel = interaction.channel;
29
+
30
+ let messages;
31
+ try {
32
+ messages = await channel.messages.fetch({ limit: 100 });
33
+ } catch (err) {
34
+ const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to fetch messages: ${err.message}`);
35
+ return interaction.reply({ embeds: [embed], ephemeral: true });
36
+ }
37
+
38
+ let filtered = [...messages.values()];
39
+
40
+ if (filterUser) {
41
+ filtered = filtered.filter((m) => m.author.id === filterUser.id);
42
+ }
43
+
44
+ // Take only the requested amount
45
+ filtered = filtered.slice(0, amount);
46
+
47
+ if (filtered.length === 0) {
48
+ const embed = new EmbedBuilder().setColor(0xf39c12).setDescription("No messages to delete.");
49
+ return interaction.reply({ embeds: [embed], ephemeral: true });
50
+ }
51
+
52
+ let deleted;
53
+ try {
54
+ // bulkDelete passes true to filter out messages older than 14 days
55
+ deleted = await channel.bulkDelete(filtered, true);
56
+ } catch (err) {
57
+ const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to delete messages: ${err.message}`);
58
+ return interaction.reply({ embeds: [embed], ephemeral: true });
59
+ }
60
+
61
+ const embed = new EmbedBuilder()
62
+ .setColor(0x3498db)
63
+ .setDescription(`Deleted **${deleted.size}** message(s).${filterUser ? ` (filtered to ${filterUser.tag})` : ""}`)
64
+ .setTimestamp();
65
+
66
+ await interaction.reply({ embeds: [embed], ephemeral: true });
67
+ },
68
+ };
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+
3
+ const { EmbedBuilder, PermissionFlagsBits } = require("discord.js");
4
+ const { requirePerms } = require("../lib/permissions");
5
+
6
+ module.exports = {
7
+ data: {
8
+ name: "slowmode",
9
+ description: "Set slowmode for this channel (0 to disable)",
10
+ options: [
11
+ {
12
+ type: 4,
13
+ name: "seconds",
14
+ description: "Slowmode in seconds (0-21600)",
15
+ required: true,
16
+ min_value: 0,
17
+ max_value: 21600,
18
+ },
19
+ ],
20
+ },
21
+
22
+ async execute(interaction, ctx) {
23
+ if (!requirePerms(interaction, PermissionFlagsBits.ManageChannels)) return;
24
+
25
+ const seconds = interaction.options.getInteger("seconds");
26
+ const channel = interaction.channel;
27
+
28
+ try {
29
+ await channel.setRateLimitPerUser(seconds, `Slowmode set by ${interaction.user.tag}`);
30
+ } catch (err) {
31
+ const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to set slowmode: ${err.message}`);
32
+ return interaction.reply({ embeds: [embed], ephemeral: true });
33
+ }
34
+
35
+ const embed = new EmbedBuilder()
36
+ .setColor(0x3498db)
37
+ .setDescription(
38
+ seconds === 0
39
+ ? "Slowmode **disabled** for this channel."
40
+ : `Slowmode set to **${seconds} second(s)** for this channel.`
41
+ )
42
+ .setTimestamp();
43
+
44
+ await interaction.reply({ embeds: [embed], ephemeral: true });
45
+ },
46
+ };