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 +110 -0
- package/commands/ban.js +90 -0
- package/commands/case.js +63 -0
- package/commands/clearwarnings.js +35 -0
- package/commands/history.js +46 -0
- package/commands/kick.js +79 -0
- package/commands/lock.js +36 -0
- package/commands/modstats.js +54 -0
- package/commands/note.js +58 -0
- package/commands/purge.js +68 -0
- package/commands/slowmode.js +46 -0
- package/commands/ticket.js +276 -0
- package/commands/timeout.js +93 -0
- package/commands/unban.js +62 -0
- package/commands/unlock.js +36 -0
- package/commands/untimeout.js +63 -0
- package/commands/warn.js +75 -0
- package/commands/warnings.js +45 -0
- package/index.js +56 -0
- package/lib/checkThresholds.js +80 -0
- package/lib/dmUser.js +31 -0
- package/lib/logCase.js +82 -0
- package/lib/parseDuration.js +31 -0
- package/lib/permissions.js +33 -0
- package/models/case.js +21 -0
- package/models/note.js +13 -0
- package/models/ticket.js +15 -0
- package/package.json +13 -0
- package/plugin.json +29 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { EmbedBuilder, PermissionFlagsBits, ChannelType } = require("discord.js");
|
|
4
|
+
const { requirePerms } = require("../lib/permissions");
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
data: {
|
|
8
|
+
name: "ticket",
|
|
9
|
+
description: "Ticket system management",
|
|
10
|
+
options: [
|
|
11
|
+
{
|
|
12
|
+
type: 1, // SUB_COMMAND
|
|
13
|
+
name: "setup",
|
|
14
|
+
description: "Configure the ticket system",
|
|
15
|
+
options: [
|
|
16
|
+
{ type: 7, name: "category", description: "Category for ticket channels", required: true },
|
|
17
|
+
{ type: 8, name: "support_role", description: "Role pinged when a ticket opens", required: true },
|
|
18
|
+
{ type: 7, name: "log_channel", description: "Channel for ticket transcripts", required: false },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: 1, // SUB_COMMAND
|
|
23
|
+
name: "open",
|
|
24
|
+
description: "Open a support ticket",
|
|
25
|
+
options: [
|
|
26
|
+
{ type: 3, name: "reason", description: "Reason for opening a ticket", required: false },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: 1, // SUB_COMMAND
|
|
31
|
+
name: "close",
|
|
32
|
+
description: "Close the current ticket",
|
|
33
|
+
options: [],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 1, // SUB_COMMAND
|
|
37
|
+
name: "add",
|
|
38
|
+
description: "Add a user to this ticket",
|
|
39
|
+
options: [
|
|
40
|
+
{ type: 6, name: "user", description: "User to add", required: true },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 1, // SUB_COMMAND
|
|
45
|
+
name: "remove",
|
|
46
|
+
description: "Remove a user from this ticket",
|
|
47
|
+
options: [
|
|
48
|
+
{ type: 6, name: "user", description: "User to remove", required: true },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async execute(interaction, ctx) {
|
|
55
|
+
const sub = interaction.options.getSubcommand();
|
|
56
|
+
const guildId = interaction.guild.id;
|
|
57
|
+
const TicketModel = ctx.models.Ticket;
|
|
58
|
+
|
|
59
|
+
if (sub === "setup") {
|
|
60
|
+
if (!requirePerms(interaction, PermissionFlagsBits.ManageGuild)) return;
|
|
61
|
+
|
|
62
|
+
const category = interaction.options.getChannel("category");
|
|
63
|
+
const supportRole = interaction.options.getRole ? interaction.options.getRole("support_role") : null;
|
|
64
|
+
const logChannel = interaction.options.getChannel("log_channel");
|
|
65
|
+
|
|
66
|
+
// For raw option access when getRole isn't available (mock environment)
|
|
67
|
+
const supportRoleId = supportRole
|
|
68
|
+
? supportRole.id
|
|
69
|
+
: interaction.options.get("support_role")?.value || null;
|
|
70
|
+
|
|
71
|
+
await ctx.db.updatePluginConfig(guildId, "adb-plugin-moderation", {
|
|
72
|
+
ticket_category_id: category.id,
|
|
73
|
+
ticket_support_role_id: supportRoleId,
|
|
74
|
+
ticket_log_channel_id: logChannel ? logChannel.id : null,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const embed = new EmbedBuilder()
|
|
78
|
+
.setColor(0x2ecc71)
|
|
79
|
+
.setTitle("Ticket System Configured")
|
|
80
|
+
.addFields(
|
|
81
|
+
{ name: "Category", value: `${category}`, inline: true },
|
|
82
|
+
{ name: "Support Role", value: supportRoleId ? `<@&${supportRoleId}>` : "None", inline: true },
|
|
83
|
+
{ name: "Log Channel", value: logChannel ? `${logChannel}` : "None", inline: true }
|
|
84
|
+
)
|
|
85
|
+
.setTimestamp();
|
|
86
|
+
|
|
87
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (sub === "open") {
|
|
91
|
+
const reason = interaction.options.getString("reason") || "";
|
|
92
|
+
const configData = (await ctx.db.getPluginConfig(guildId, "adb-plugin-moderation"))?.data || {};
|
|
93
|
+
|
|
94
|
+
if (!configData.ticket_category_id) {
|
|
95
|
+
const embed = new EmbedBuilder()
|
|
96
|
+
.setColor(0xe74c3c)
|
|
97
|
+
.setDescription("Ticket system is not configured. Ask an admin to run `/ticket setup` first.");
|
|
98
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if user already has an open ticket
|
|
102
|
+
const existing = await TicketModel.findOne({
|
|
103
|
+
guildId,
|
|
104
|
+
userId: interaction.user.id,
|
|
105
|
+
status: "open",
|
|
106
|
+
}).lean();
|
|
107
|
+
|
|
108
|
+
if (existing) {
|
|
109
|
+
const embed = new EmbedBuilder()
|
|
110
|
+
.setColor(0xe74c3c)
|
|
111
|
+
.setDescription(`You already have an open ticket: <#${existing.channelId}>`);
|
|
112
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let ticketChannel;
|
|
116
|
+
try {
|
|
117
|
+
ticketChannel = await interaction.guild.channels.create({
|
|
118
|
+
name: `ticket-${interaction.user.username}`,
|
|
119
|
+
type: ChannelType.GuildText,
|
|
120
|
+
parent: configData.ticket_category_id,
|
|
121
|
+
permissionOverwrites: [
|
|
122
|
+
{
|
|
123
|
+
id: interaction.guild.roles.everyone.id,
|
|
124
|
+
deny: [PermissionFlagsBits.ViewChannel],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: interaction.user.id,
|
|
128
|
+
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages],
|
|
129
|
+
},
|
|
130
|
+
...(configData.ticket_support_role_id
|
|
131
|
+
? [
|
|
132
|
+
{
|
|
133
|
+
id: configData.ticket_support_role_id,
|
|
134
|
+
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages],
|
|
135
|
+
},
|
|
136
|
+
]
|
|
137
|
+
: []),
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to create ticket channel: ${err.message}`);
|
|
142
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const ticketDoc = new TicketModel({
|
|
146
|
+
guildId,
|
|
147
|
+
channelId: ticketChannel.id,
|
|
148
|
+
userId: interaction.user.id,
|
|
149
|
+
reason,
|
|
150
|
+
status: "open",
|
|
151
|
+
});
|
|
152
|
+
await ticketDoc.save();
|
|
153
|
+
|
|
154
|
+
const openEmbed = new EmbedBuilder()
|
|
155
|
+
.setColor(0x3498db)
|
|
156
|
+
.setTitle("Support Ticket Opened")
|
|
157
|
+
.setDescription(`Hello ${interaction.user}, a staff member will be with you shortly.`)
|
|
158
|
+
.addFields(reason ? [{ name: "Reason", value: reason }] : [])
|
|
159
|
+
.setTimestamp();
|
|
160
|
+
|
|
161
|
+
const pingContent = configData.ticket_support_role_id
|
|
162
|
+
? `<@&${configData.ticket_support_role_id}>`
|
|
163
|
+
: null;
|
|
164
|
+
|
|
165
|
+
await ticketChannel.send({
|
|
166
|
+
content: pingContent,
|
|
167
|
+
embeds: [openEmbed],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const confirmEmbed = new EmbedBuilder()
|
|
171
|
+
.setColor(0x2ecc71)
|
|
172
|
+
.setDescription(`Your ticket has been opened: ${ticketChannel}`);
|
|
173
|
+
|
|
174
|
+
return interaction.reply({ embeds: [confirmEmbed], ephemeral: true });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (sub === "close") {
|
|
178
|
+
if (!requirePerms(interaction, PermissionFlagsBits.ManageChannels)) return;
|
|
179
|
+
|
|
180
|
+
const ticketDoc = await TicketModel.findOne({
|
|
181
|
+
guildId,
|
|
182
|
+
channelId: interaction.channel.id,
|
|
183
|
+
status: "open",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (!ticketDoc) {
|
|
187
|
+
const embed = new EmbedBuilder()
|
|
188
|
+
.setColor(0xe74c3c)
|
|
189
|
+
.setDescription("This channel is not an open ticket.");
|
|
190
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const configData = (await ctx.db.getPluginConfig(guildId, "adb-plugin-moderation"))?.data || {};
|
|
194
|
+
|
|
195
|
+
// Post transcript to log channel
|
|
196
|
+
if (configData.ticket_log_channel_id) {
|
|
197
|
+
try {
|
|
198
|
+
const logChannel = await ctx.client.channels.fetch(configData.ticket_log_channel_id);
|
|
199
|
+
if (logChannel && logChannel.isTextBased()) {
|
|
200
|
+
const messages = await interaction.channel.messages.fetch({ limit: 100 });
|
|
201
|
+
const transcript = [...messages.values()]
|
|
202
|
+
.reverse()
|
|
203
|
+
.map((m) => `[${new Date(m.createdTimestamp).toISOString()}] ${m.author.tag}: ${m.content}`)
|
|
204
|
+
.join("\n");
|
|
205
|
+
|
|
206
|
+
const logEmbed = new EmbedBuilder()
|
|
207
|
+
.setColor(0x95a5a6)
|
|
208
|
+
.setTitle("Ticket Closed")
|
|
209
|
+
.addFields(
|
|
210
|
+
{ name: "Opened by", value: `<@${ticketDoc.userId}>`, inline: true },
|
|
211
|
+
{ name: "Closed by", value: `${interaction.user.tag}`, inline: true },
|
|
212
|
+
{ name: "Reason", value: ticketDoc.reason || "No reason" }
|
|
213
|
+
)
|
|
214
|
+
.setTimestamp();
|
|
215
|
+
|
|
216
|
+
await logChannel.send({ embeds: [logEmbed] });
|
|
217
|
+
|
|
218
|
+
if (transcript.length > 0) {
|
|
219
|
+
const truncated = transcript.length > 1900 ? transcript.slice(-1900) + "\n..." : transcript;
|
|
220
|
+
await logChannel.send({ content: `\`\`\`\n${truncated}\n\`\`\`` });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
ctx.logger.warn(`[moderation] Failed to log ticket transcript: ${err.message}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
ticketDoc.status = "closed";
|
|
229
|
+
ticketDoc.closedAt = new Date();
|
|
230
|
+
await ticketDoc.save();
|
|
231
|
+
|
|
232
|
+
await interaction.reply({ content: "Ticket closed. Deleting channel in 5 seconds...", ephemeral: false });
|
|
233
|
+
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
interaction.channel.delete("Ticket closed").catch(() => {});
|
|
236
|
+
}, 5000);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (sub === "add") {
|
|
240
|
+
if (!requirePerms(interaction, PermissionFlagsBits.ManageChannels)) return;
|
|
241
|
+
|
|
242
|
+
const user = interaction.options.getUser("user");
|
|
243
|
+
try {
|
|
244
|
+
await interaction.channel.permissionOverwrites.edit(user.id, {
|
|
245
|
+
ViewChannel: true,
|
|
246
|
+
SendMessages: true,
|
|
247
|
+
});
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to add user: ${err.message}`);
|
|
250
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const embed = new EmbedBuilder()
|
|
254
|
+
.setColor(0x2ecc71)
|
|
255
|
+
.setDescription(`Added ${user} to this ticket.`);
|
|
256
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (sub === "remove") {
|
|
260
|
+
if (!requirePerms(interaction, PermissionFlagsBits.ManageChannels)) return;
|
|
261
|
+
|
|
262
|
+
const user = interaction.options.getUser("user");
|
|
263
|
+
try {
|
|
264
|
+
await interaction.channel.permissionOverwrites.delete(user.id);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to remove user: ${err.message}`);
|
|
267
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const embed = new EmbedBuilder()
|
|
271
|
+
.setColor(0xe67e22)
|
|
272
|
+
.setDescription(`Removed ${user} from this ticket.`);
|
|
273
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { EmbedBuilder, PermissionFlagsBits } = require("discord.js");
|
|
4
|
+
const { requirePerms } = require("../lib/permissions");
|
|
5
|
+
const { parseDuration } = require("../lib/parseDuration");
|
|
6
|
+
const { dmActionUser } = require("../lib/dmUser");
|
|
7
|
+
const { createCase, postCaseLog } = require("../lib/logCase");
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
data: {
|
|
11
|
+
name: "timeout",
|
|
12
|
+
description: "Timeout a member (e.g. 1h, 30m, 7d)",
|
|
13
|
+
options: [
|
|
14
|
+
{ type: 6, name: "user", description: "The member to timeout", required: true },
|
|
15
|
+
{ type: 3, name: "duration", description: "Duration (e.g. 30m, 1h, 7d — max 28d)", required: true },
|
|
16
|
+
{ type: 3, name: "reason", description: "Reason for the timeout", required: false },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async execute(interaction, ctx) {
|
|
21
|
+
if (!requirePerms(interaction, PermissionFlagsBits.ModerateMembers)) return;
|
|
22
|
+
|
|
23
|
+
const targetUser = interaction.options.getUser("user");
|
|
24
|
+
const durationStr = interaction.options.getString("duration");
|
|
25
|
+
const reason = interaction.options.getString("reason") || "No reason provided";
|
|
26
|
+
const guildId = interaction.guild.id;
|
|
27
|
+
|
|
28
|
+
const ms = parseDuration(durationStr);
|
|
29
|
+
if (!ms) {
|
|
30
|
+
const embed = new EmbedBuilder()
|
|
31
|
+
.setColor(0xe74c3c)
|
|
32
|
+
.setDescription("Invalid duration. Use formats like `30m`, `1h`, `7d`. Maximum is 28 days.");
|
|
33
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CaseModel = ctx.models.Case;
|
|
37
|
+
const configData = (await ctx.db.getPluginConfig(guildId, "adb-plugin-moderation"))?.data || {};
|
|
38
|
+
|
|
39
|
+
let member;
|
|
40
|
+
try {
|
|
41
|
+
member = await interaction.guild.members.fetch(targetUser.id);
|
|
42
|
+
} catch {
|
|
43
|
+
const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription("That user is not in this server.");
|
|
44
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!member.moderatable) {
|
|
48
|
+
const embed = new EmbedBuilder()
|
|
49
|
+
.setColor(0xe74c3c)
|
|
50
|
+
.setDescription("I cannot timeout that member (insufficient role hierarchy).");
|
|
51
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (configData.dm_on_action !== false) {
|
|
55
|
+
await dmActionUser(ctx.client, targetUser.id, {
|
|
56
|
+
action: "timed out",
|
|
57
|
+
guildName: interaction.guild.name,
|
|
58
|
+
reason,
|
|
59
|
+
duration: durationStr,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await member.timeout(ms, reason);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to timeout: ${err.message}`);
|
|
67
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const caseDoc = await createCase(CaseModel, {
|
|
71
|
+
guildId,
|
|
72
|
+
type: "timeout",
|
|
73
|
+
targetId: targetUser.id,
|
|
74
|
+
moderatorId: interaction.user.id,
|
|
75
|
+
reason,
|
|
76
|
+
duration: durationStr,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await postCaseLog(ctx, configData, caseDoc, targetUser, interaction.user);
|
|
80
|
+
|
|
81
|
+
const embed = new EmbedBuilder()
|
|
82
|
+
.setColor(0xf39c12)
|
|
83
|
+
.setTitle(`Timed Out — Case #${caseDoc.caseNumber}`)
|
|
84
|
+
.addFields(
|
|
85
|
+
{ name: "User", value: `${targetUser.tag} (${targetUser.id})`, inline: true },
|
|
86
|
+
{ name: "Duration", value: durationStr, inline: true },
|
|
87
|
+
{ name: "Reason", value: reason }
|
|
88
|
+
)
|
|
89
|
+
.setTimestamp();
|
|
90
|
+
|
|
91
|
+
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
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: "unban",
|
|
10
|
+
description: "Unban a user by ID",
|
|
11
|
+
options: [
|
|
12
|
+
{ type: 3, name: "user_id", description: "The user ID to unban", required: true },
|
|
13
|
+
{ type: 3, name: "reason", description: "Reason for the unban", required: false },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
async execute(interaction, ctx) {
|
|
18
|
+
if (!requirePerms(interaction, PermissionFlagsBits.BanMembers)) return;
|
|
19
|
+
|
|
20
|
+
const userId = interaction.options.getString("user_id");
|
|
21
|
+
const reason = interaction.options.getString("reason") || "No reason provided";
|
|
22
|
+
const guildId = interaction.guild.id;
|
|
23
|
+
|
|
24
|
+
const CaseModel = ctx.models.Case;
|
|
25
|
+
const configData = (await ctx.db.getPluginConfig(guildId, "adb-plugin-moderation"))?.data || {};
|
|
26
|
+
|
|
27
|
+
let targetUser = null;
|
|
28
|
+
try {
|
|
29
|
+
targetUser = await ctx.client.users.fetch(userId);
|
|
30
|
+
} catch {
|
|
31
|
+
// May not be resolvable — continue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await interaction.guild.bans.remove(userId, reason);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to unban: ${err.message}`);
|
|
38
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const caseDoc = await createCase(CaseModel, {
|
|
42
|
+
guildId,
|
|
43
|
+
type: "unban",
|
|
44
|
+
targetId: userId,
|
|
45
|
+
moderatorId: interaction.user.id,
|
|
46
|
+
reason,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await postCaseLog(ctx, configData, caseDoc, targetUser, interaction.user);
|
|
50
|
+
|
|
51
|
+
const embed = new EmbedBuilder()
|
|
52
|
+
.setColor(0x2ecc71)
|
|
53
|
+
.setTitle(`Unbanned — Case #${caseDoc.caseNumber}`)
|
|
54
|
+
.addFields(
|
|
55
|
+
{ name: "User", value: targetUser ? `${targetUser.tag} (${userId})` : userId, inline: true },
|
|
56
|
+
{ name: "Reason", value: reason }
|
|
57
|
+
)
|
|
58
|
+
.setTimestamp();
|
|
59
|
+
|
|
60
|
+
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -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: "unlock",
|
|
9
|
+
description: "Unlock a channel (restore @everyone send permission)",
|
|
10
|
+
options: [
|
|
11
|
+
{ type: 7, name: "channel", description: "Channel to unlock (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: null, // Reset to role default
|
|
23
|
+
});
|
|
24
|
+
} catch (err) {
|
|
25
|
+
const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to unlock channel: ${err.message}`);
|
|
26
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const embed = new EmbedBuilder()
|
|
30
|
+
.setColor(0x2ecc71)
|
|
31
|
+
.setDescription(`${target} has been **unlocked**. Members can send messages again.`)
|
|
32
|
+
.setTimestamp();
|
|
33
|
+
|
|
34
|
+
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
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: "untimeout",
|
|
10
|
+
description: "Remove a timeout from a member",
|
|
11
|
+
options: [
|
|
12
|
+
{ type: 6, name: "user", description: "The member to untimeout", required: true },
|
|
13
|
+
{ type: 3, name: "reason", description: "Reason", required: false },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
async execute(interaction, ctx) {
|
|
18
|
+
if (!requirePerms(interaction, PermissionFlagsBits.ModerateMembers)) return;
|
|
19
|
+
|
|
20
|
+
const targetUser = interaction.options.getUser("user");
|
|
21
|
+
const reason = interaction.options.getString("reason") || "No reason provided";
|
|
22
|
+
const guildId = interaction.guild.id;
|
|
23
|
+
|
|
24
|
+
const CaseModel = ctx.models.Case;
|
|
25
|
+
const configData = (await ctx.db.getPluginConfig(guildId, "adb-plugin-moderation"))?.data || {};
|
|
26
|
+
|
|
27
|
+
let member;
|
|
28
|
+
try {
|
|
29
|
+
member = await interaction.guild.members.fetch(targetUser.id);
|
|
30
|
+
} catch {
|
|
31
|
+
const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription("That user is not in this server.");
|
|
32
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await member.timeout(null, reason);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const embed = new EmbedBuilder().setColor(0xe74c3c).setDescription(`Failed to remove timeout: ${err.message}`);
|
|
39
|
+
return interaction.reply({ embeds: [embed], ephemeral: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const caseDoc = await createCase(CaseModel, {
|
|
43
|
+
guildId,
|
|
44
|
+
type: "untimeout",
|
|
45
|
+
targetId: targetUser.id,
|
|
46
|
+
moderatorId: interaction.user.id,
|
|
47
|
+
reason,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await postCaseLog(ctx, configData, caseDoc, targetUser, interaction.user);
|
|
51
|
+
|
|
52
|
+
const embed = new EmbedBuilder()
|
|
53
|
+
.setColor(0x3498db)
|
|
54
|
+
.setTitle(`Timeout Removed — Case #${caseDoc.caseNumber}`)
|
|
55
|
+
.addFields(
|
|
56
|
+
{ name: "User", value: `${targetUser.tag} (${targetUser.id})`, inline: true },
|
|
57
|
+
{ name: "Reason", value: reason }
|
|
58
|
+
)
|
|
59
|
+
.setTimestamp();
|
|
60
|
+
|
|
61
|
+
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
62
|
+
},
|
|
63
|
+
};
|
package/commands/warn.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
const { checkWarnThresholds } = require("../lib/checkThresholds");
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
data: {
|
|
11
|
+
name: "warn",
|
|
12
|
+
description: "Warn a member",
|
|
13
|
+
options: [
|
|
14
|
+
{ type: 6, name: "user", description: "The member to warn", required: true },
|
|
15
|
+
{ type: 3, name: "reason", description: "Reason for the warning", required: true },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
async execute(interaction, ctx) {
|
|
20
|
+
if (!requirePerms(interaction, PermissionFlagsBits.ManageMessages)) return;
|
|
21
|
+
|
|
22
|
+
const targetUser = interaction.options.getUser("user");
|
|
23
|
+
const reason = interaction.options.getString("reason");
|
|
24
|
+
const guildId = interaction.guild.id;
|
|
25
|
+
|
|
26
|
+
const CaseModel = ctx.models.Case;
|
|
27
|
+
const configData = (await ctx.db.getPluginConfig(guildId, "adb-plugin-moderation"))?.data || {};
|
|
28
|
+
|
|
29
|
+
const caseDoc = await createCase(CaseModel, {
|
|
30
|
+
guildId,
|
|
31
|
+
type: "warn",
|
|
32
|
+
targetId: targetUser.id,
|
|
33
|
+
moderatorId: interaction.user.id,
|
|
34
|
+
reason,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Increment warning counter in user profile
|
|
38
|
+
const profile = await ctx.db.getUserProfile(targetUser.id, guildId);
|
|
39
|
+
const newWarnings = (profile.warnings || 0) + 1;
|
|
40
|
+
await ctx.db.updateUserProfile(targetUser.id, guildId, { warnings: newWarnings });
|
|
41
|
+
|
|
42
|
+
if (configData.dm_on_action !== false) {
|
|
43
|
+
await dmActionUser(ctx.client, targetUser.id, {
|
|
44
|
+
action: "warned",
|
|
45
|
+
guildName: interaction.guild.name,
|
|
46
|
+
reason,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await postCaseLog(ctx, configData, caseDoc, targetUser, interaction.user);
|
|
51
|
+
|
|
52
|
+
const embed = new EmbedBuilder()
|
|
53
|
+
.setColor(0xf1c40f)
|
|
54
|
+
.setTitle(`Warned — Case #${caseDoc.caseNumber}`)
|
|
55
|
+
.addFields(
|
|
56
|
+
{ name: "User", value: `${targetUser.tag} (${targetUser.id})`, inline: true },
|
|
57
|
+
{ name: "Total Warnings", value: String(newWarnings), inline: true },
|
|
58
|
+
{ name: "Reason", value: reason }
|
|
59
|
+
)
|
|
60
|
+
.setTimestamp();
|
|
61
|
+
|
|
62
|
+
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
63
|
+
|
|
64
|
+
// Check thresholds AFTER replying so auto-actions don't block the response
|
|
65
|
+
await checkWarnThresholds(
|
|
66
|
+
ctx,
|
|
67
|
+
interaction,
|
|
68
|
+
CaseModel,
|
|
69
|
+
targetUser.id,
|
|
70
|
+
guildId,
|
|
71
|
+
newWarnings,
|
|
72
|
+
configData.warn_thresholds
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
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: "warnings",
|
|
9
|
+
description: "View warnings for a member",
|
|
10
|
+
options: [
|
|
11
|
+
{ type: 6, name: "user", description: "The member to check", 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 warns = await CaseModel.find({ guildId, targetUserId: targetUser.id, type: "warn" })
|
|
23
|
+
.sort({ createdAt: -1 })
|
|
24
|
+
.limit(25)
|
|
25
|
+
.lean();
|
|
26
|
+
|
|
27
|
+
const embed = new EmbedBuilder()
|
|
28
|
+
.setColor(0xf1c40f)
|
|
29
|
+
.setTitle(`Warnings for ${targetUser.tag}`)
|
|
30
|
+
.setDescription(
|
|
31
|
+
warns.length === 0
|
|
32
|
+
? "No warnings on record."
|
|
33
|
+
: warns
|
|
34
|
+
.map(
|
|
35
|
+
(w) =>
|
|
36
|
+
`**Case #${w.caseNumber}** — <t:${Math.floor(new Date(w.createdAt).getTime() / 1000)}:R>\n${w.reason}`
|
|
37
|
+
)
|
|
38
|
+
.join("\n\n")
|
|
39
|
+
)
|
|
40
|
+
.setFooter({ text: `Total: ${warns.length} warning(s)` })
|
|
41
|
+
.setTimestamp();
|
|
42
|
+
|
|
43
|
+
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
44
|
+
},
|
|
45
|
+
};
|