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/index.js ADDED
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ const CaseSchema = require("./models/case");
4
+ const NoteSchema = require("./models/note");
5
+ const TicketSchema = require("./models/ticket");
6
+
7
+ const commands = [
8
+ require("./commands/ban"),
9
+ require("./commands/unban"),
10
+ require("./commands/kick"),
11
+ require("./commands/timeout"),
12
+ require("./commands/untimeout"),
13
+ require("./commands/warn"),
14
+ require("./commands/warnings"),
15
+ require("./commands/clearwarnings"),
16
+ require("./commands/note"),
17
+ require("./commands/purge"),
18
+ require("./commands/slowmode"),
19
+ require("./commands/lock"),
20
+ require("./commands/unlock"),
21
+ require("./commands/case"),
22
+ require("./commands/history"),
23
+ require("./commands/modstats"),
24
+ require("./commands/ticket"),
25
+ ];
26
+
27
+ /**
28
+ * Plugin entry point called by the ADB plugin loader.
29
+ * @param {object} ctx — plugin context
30
+ */
31
+ async function load(ctx) {
32
+ // Define mongoose models (namespaced internally by ADB)
33
+ const CaseModel = ctx.defineModel("Case", CaseSchema);
34
+ const NoteModel = ctx.defineModel("Note", NoteSchema);
35
+ const TicketModel = ctx.defineModel("Ticket", TicketSchema);
36
+
37
+ // Attach models to ctx so commands can access them via ctx.models
38
+ ctx.models = { Case: CaseModel, Note: NoteModel, Ticket: TicketModel };
39
+
40
+ // Register all slash commands
41
+ for (const cmd of commands) {
42
+ ctx.registerCommand({
43
+ data: cmd.data,
44
+ execute: (interaction) => cmd.execute(interaction, ctx),
45
+ });
46
+ }
47
+
48
+ // Cleanup hook
49
+ ctx.hooks.on("onPluginUnload", () => {
50
+ ctx.logger.info("[moderation] Plugin unloaded.");
51
+ });
52
+
53
+ ctx.logger.info("[moderation] Moderation plugin loaded — registered " + commands.length + " commands.");
54
+ }
55
+
56
+ module.exports = { load };
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+
3
+ const { parseDuration } = require("./parseDuration");
4
+ const { dmActionUser } = require("./dmUser");
5
+ const { createCase, postCaseLog } = require("./logCase");
6
+
7
+ /**
8
+ * Apply an automatic action if the warning count hits a configured threshold.
9
+ *
10
+ * @param {object} ctx
11
+ * @param {import("discord.js").ChatInputCommandInteraction} interaction
12
+ * @param {import("mongoose").Model} CaseModel
13
+ * @param {string} userId
14
+ * @param {string} guildId
15
+ * @param {number} warningCount
16
+ * @param {object} thresholds — map of "N": { action, duration? }
17
+ */
18
+ async function checkWarnThresholds(ctx, interaction, CaseModel, userId, guildId, warningCount, thresholds) {
19
+ if (!thresholds) return;
20
+
21
+ const threshold = thresholds[String(warningCount)];
22
+ if (!threshold) return;
23
+
24
+ const { action, duration: durationStr } = threshold;
25
+ const guild = interaction.guild;
26
+ const configData = (await ctx.db.getPluginConfig(guildId, "adb-plugin-moderation"))?.data || {};
27
+
28
+ let member;
29
+ try {
30
+ member = await guild.members.fetch(userId);
31
+ } catch {
32
+ return; // Member may have left
33
+ }
34
+
35
+ const reason = `Auto-action: ${warningCount} warnings reached`;
36
+
37
+ try {
38
+ if (action === "ban") {
39
+ await dmActionUser(ctx.client, userId, { action: "banned", guildName: guild.name, reason });
40
+ await member.ban({ reason });
41
+ const caseDoc = await createCase(CaseModel, {
42
+ guildId,
43
+ type: "ban",
44
+ targetId: userId,
45
+ moderatorId: ctx.client.user.id,
46
+ reason,
47
+ });
48
+ await postCaseLog(ctx, configData, caseDoc, await ctx.client.users.fetch(userId).catch(() => null), ctx.client.user);
49
+ } else if (action === "kick") {
50
+ await dmActionUser(ctx.client, userId, { action: "kicked", guildName: guild.name, reason });
51
+ await member.kick(reason);
52
+ const caseDoc = await createCase(CaseModel, {
53
+ guildId,
54
+ type: "kick",
55
+ targetId: userId,
56
+ moderatorId: ctx.client.user.id,
57
+ reason,
58
+ });
59
+ await postCaseLog(ctx, configData, caseDoc, await ctx.client.users.fetch(userId).catch(() => null), ctx.client.user);
60
+ } else if (action === "timeout") {
61
+ const ms = durationStr ? parseDuration(durationStr) : 3600000;
62
+ if (!ms) return;
63
+ await member.timeout(ms, reason);
64
+ await dmActionUser(ctx.client, userId, { action: "timed out", guildName: guild.name, reason, duration: durationStr });
65
+ const caseDoc = await createCase(CaseModel, {
66
+ guildId,
67
+ type: "timeout",
68
+ targetId: userId,
69
+ moderatorId: ctx.client.user.id,
70
+ reason,
71
+ duration: durationStr,
72
+ });
73
+ await postCaseLog(ctx, configData, caseDoc, await ctx.client.users.fetch(userId).catch(() => null), ctx.client.user);
74
+ }
75
+ } catch (err) {
76
+ ctx.logger.warn(`[moderation] Auto-threshold action failed: ${err.message}`);
77
+ }
78
+ }
79
+
80
+ module.exports = { checkWarnThresholds };
package/lib/dmUser.js ADDED
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+
3
+ const { EmbedBuilder } = require("discord.js");
4
+
5
+ /**
6
+ * DM a user about a moderation action. Silently swallows failures.
7
+ * @param {import("discord.js").Client} client
8
+ * @param {string} userId
9
+ * @param {{ action: string, guildName: string, reason?: string, duration?: string }} opts
10
+ */
11
+ async function dmActionUser(client, userId, { action, guildName, reason, duration }) {
12
+ try {
13
+ const user = await client.users.fetch(userId);
14
+ const desc = duration
15
+ ? `You have been **${action}** from **${guildName}** for **${duration}**.`
16
+ : `You have been **${action}** from **${guildName}**.`;
17
+
18
+ const embed = new EmbedBuilder()
19
+ .setColor(0xe67e22)
20
+ .setTitle(`Moderation Action — ${action.charAt(0).toUpperCase() + action.slice(1)}`)
21
+ .setDescription(desc)
22
+ .addFields({ name: "Reason", value: reason || "No reason provided" })
23
+ .setTimestamp();
24
+
25
+ await user.send({ embeds: [embed] });
26
+ } catch {
27
+ // Silently ignore — user may have DMs closed or bot cannot reach them
28
+ }
29
+ }
30
+
31
+ module.exports = { dmActionUser };
package/lib/logCase.js ADDED
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ const { EmbedBuilder } = require("discord.js");
4
+
5
+ const TYPE_COLORS = {
6
+ ban: 0xe74c3c,
7
+ unban: 0x2ecc71,
8
+ kick: 0xe67e22,
9
+ timeout: 0xf39c12,
10
+ untimeout: 0x3498db,
11
+ warn: 0xf1c40f,
12
+ note: 0x95a5a6,
13
+ };
14
+
15
+ /**
16
+ * Create a new moderation case document and save it.
17
+ * Case numbers are per-guild, starting from 1.
18
+ *
19
+ * @param {import("mongoose").Model} CaseModel
20
+ * @param {{ guildId: string, type: string, targetId: string, moderatorId: string, reason?: string, duration?: string }} opts
21
+ * @returns {Promise<object>} Saved case document
22
+ */
23
+ async function createCase(CaseModel, { guildId, type, targetId, moderatorId, reason, duration }) {
24
+ const last = await CaseModel.findOne({ guildId }).sort({ caseNumber: -1 }).select("caseNumber").lean();
25
+ const caseNumber = last ? last.caseNumber + 1 : 1;
26
+
27
+ const doc = new CaseModel({
28
+ guildId,
29
+ caseNumber,
30
+ type,
31
+ targetUserId: targetId,
32
+ moderatorId,
33
+ reason: reason || "No reason provided",
34
+ duration: duration || null,
35
+ });
36
+
37
+ await doc.save();
38
+ return doc;
39
+ }
40
+
41
+ /**
42
+ * Post a case embed to the configured log channel.
43
+ *
44
+ * @param {object} ctx — plugin context
45
+ * @param {object} configData — guild plugin config data
46
+ * @param {object} caseDoc — saved case document
47
+ * @param {import("discord.js").User|null} targetUser
48
+ * @param {import("discord.js").User|null} moderator
49
+ */
50
+ async function postCaseLog(ctx, configData, caseDoc, targetUser, moderator) {
51
+ const logChannelId = configData && configData.log_channel_id;
52
+ if (!logChannelId) return;
53
+
54
+ try {
55
+ const channel = await ctx.client.channels.fetch(logChannelId);
56
+ if (!channel || !channel.isTextBased()) return;
57
+
58
+ const color = TYPE_COLORS[caseDoc.type] || 0x99aab5;
59
+ const targetName = targetUser ? `${targetUser.tag} (${targetUser.id})` : caseDoc.targetUserId;
60
+ const modName = moderator ? `${moderator.tag} (${moderator.id})` : caseDoc.moderatorId;
61
+
62
+ const embed = new EmbedBuilder()
63
+ .setColor(color)
64
+ .setTitle(`Case #${caseDoc.caseNumber} — ${caseDoc.type.toUpperCase()}`)
65
+ .addFields(
66
+ { name: "Target", value: targetName, inline: true },
67
+ { name: "Moderator", value: modName, inline: true },
68
+ { name: "Reason", value: caseDoc.reason || "No reason provided" }
69
+ )
70
+ .setTimestamp(caseDoc.createdAt);
71
+
72
+ if (caseDoc.duration) {
73
+ embed.addFields({ name: "Duration", value: caseDoc.duration, inline: true });
74
+ }
75
+
76
+ await channel.send({ embeds: [embed] });
77
+ } catch (err) {
78
+ ctx.logger.warn(`[moderation] Failed to post case log: ${err.message}`);
79
+ }
80
+ }
81
+
82
+ module.exports = { createCase, postCaseLog };
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+
3
+ const MAX_MS = 2419200000; // 28 days
4
+
5
+ const UNITS = {
6
+ s: 1000,
7
+ m: 60 * 1000,
8
+ h: 60 * 60 * 1000,
9
+ d: 24 * 60 * 60 * 1000,
10
+ };
11
+
12
+ /**
13
+ * Parse a human-readable duration string to milliseconds.
14
+ * Accepts: 30s, 1m, 30m, 1h, 12h, 1d, 7d, 28d
15
+ * Returns null if invalid or exceeds 28 days.
16
+ * @param {string} str
17
+ * @returns {number|null}
18
+ */
19
+ function parseDuration(str) {
20
+ if (typeof str !== "string") return null;
21
+ const match = str.trim().match(/^(\d+)([smhd])$/i);
22
+ if (!match) return null;
23
+ const amount = parseInt(match[1], 10);
24
+ const unit = match[2].toLowerCase();
25
+ if (!UNITS[unit]) return null;
26
+ const ms = amount * UNITS[unit];
27
+ if (ms <= 0 || ms > MAX_MS) return null;
28
+ return ms;
29
+ }
30
+
31
+ module.exports = { parseDuration };
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+
3
+ const { PermissionFlagsBits, EmbedBuilder } = require("discord.js");
4
+
5
+ /**
6
+ * Check that interaction.member has all listed permissions.
7
+ * Replies with an ephemeral error embed if not. Returns true/false.
8
+ * @param {import("discord.js").ChatInputCommandInteraction} interaction
9
+ * @param {...bigint} perms — PermissionFlagsBits values
10
+ * @returns {boolean}
11
+ */
12
+ function requirePerms(interaction, ...perms) {
13
+ const member = interaction.member;
14
+ const missing = perms.filter((p) => !member.permissions.has(p));
15
+ if (missing.length === 0) return true;
16
+
17
+ const names = missing
18
+ .map((p) => {
19
+ const entry = Object.entries(PermissionFlagsBits).find(([, v]) => v === p);
20
+ return entry ? entry[0] : String(p);
21
+ })
22
+ .join(", ");
23
+
24
+ const embed = new EmbedBuilder()
25
+ .setColor(0xe74c3c)
26
+ .setTitle("Missing Permissions")
27
+ .setDescription(`You need the following permissions: **${names}**`);
28
+
29
+ interaction.reply({ embeds: [embed], ephemeral: true }).catch(() => {});
30
+ return false;
31
+ }
32
+
33
+ module.exports = { requirePerms };
package/models/case.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ const { Schema } = require("mongoose");
4
+
5
+ const CaseSchema = new Schema({
6
+ guildId: { type: String, required: true, index: true },
7
+ caseNumber: { type: Number, required: true },
8
+ type: {
9
+ type: String,
10
+ enum: ["ban", "unban", "kick", "timeout", "untimeout", "warn", "note"],
11
+ required: true,
12
+ },
13
+ targetUserId: { type: String, required: true },
14
+ moderatorId: { type: String, required: true },
15
+ reason: { type: String, default: "No reason provided" },
16
+ duration: { type: String, default: null },
17
+ active: { type: Boolean, default: true },
18
+ createdAt: { type: Date, default: Date.now },
19
+ });
20
+
21
+ module.exports = CaseSchema;
package/models/note.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ const { Schema } = require("mongoose");
4
+
5
+ const NoteSchema = new Schema({
6
+ guildId: { type: String, required: true, index: true },
7
+ userId: { type: String, required: true },
8
+ moderatorId: { type: String, required: true },
9
+ note: { type: String, required: true },
10
+ createdAt: { type: Date, default: Date.now },
11
+ });
12
+
13
+ module.exports = NoteSchema;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ const { Schema } = require("mongoose");
4
+
5
+ const TicketSchema = new Schema({
6
+ guildId: { type: String, required: true, index: true },
7
+ channelId: { type: String, required: true, unique: true },
8
+ userId: { type: String, required: true },
9
+ reason: { type: String, default: "" },
10
+ status: { type: String, enum: ["open", "closed"], default: "open" },
11
+ createdAt: { type: Date, default: Date.now },
12
+ closedAt: { type: Date, default: null },
13
+ });
14
+
15
+ module.exports = TicketSchema;
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "adb-plugin-moderation",
3
+ "version": "1.0.0",
4
+ "description": "Core moderation commands for Advanced Discord Bot",
5
+ "main": "index.js",
6
+ "files": ["index.js", "plugin.json", "commands", "models", "lib", "README.md", "LICENSE"],
7
+ "scripts": { "test": "node test/local-harness.js" },
8
+ "keywords": ["adb-plugin", "discord", "moderation"],
9
+ "author": "dead",
10
+ "license": "MIT",
11
+ "peerDependencies": { "discord.js": ">=14.0.0", "mongoose": ">=7.0.0" },
12
+ "devDependencies": { "mongoose": "^8.0.0" }
13
+ }
package/plugin.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "adb-plugin-moderation",
3
+ "displayName": "Basic Moderation",
4
+ "version": "1.0.0",
5
+ "description": "Core moderation commands: ban, kick, timeout, warn, purge, tickets, and case logging.",
6
+ "author": "dead",
7
+ "main": "index.js",
8
+ "requiresRestart": true,
9
+ "permissions": ["db.read", "db.write", "commands.register"],
10
+ "configSchema": {
11
+ "type": "object",
12
+ "properties": {
13
+ "log_channel_id": { "type": "string", "default": null },
14
+ "ticket_category_id": { "type": "string", "default": null },
15
+ "ticket_support_role_id": { "type": "string", "default": null },
16
+ "ticket_log_channel_id": { "type": "string", "default": null },
17
+ "warn_thresholds": {
18
+ "type": "object",
19
+ "default": {
20
+ "3": { "action": "timeout", "duration": "1h" },
21
+ "5": { "action": "timeout", "duration": "24h" },
22
+ "7": { "action": "kick" },
23
+ "10": { "action": "ban" }
24
+ }
25
+ },
26
+ "dm_on_action": { "type": "boolean", "default": true }
27
+ }
28
+ }
29
+ }