feat: add moderation commands for ban, kick, slowdown, and timeout
All checks were successful
CI / build (push) Successful in 9s
All checks were successful
CI / build (push) Successful in 9s
This commit is contained in:
110
src/listeners/messageCreate/moderation/ban.ts
Normal file
110
src/listeners/messageCreate/moderation/ban.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
Client,
|
||||
ContainerBuilder,
|
||||
Events,
|
||||
Message,
|
||||
MessageFlags,
|
||||
PermissionFlagsBits,
|
||||
SeparatorBuilder,
|
||||
SeparatorSpacingSize,
|
||||
TextDisplayBuilder,
|
||||
} from "discord.js";
|
||||
|
||||
async function sendResponse(
|
||||
message: Message,
|
||||
color: number,
|
||||
title: string,
|
||||
body: string,
|
||||
): Promise<void> {
|
||||
const container = new ContainerBuilder().setAccentColor(color);
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(`## ${title}`),
|
||||
);
|
||||
container.addSeparatorComponents(
|
||||
new SeparatorBuilder()
|
||||
.setDivider(true)
|
||||
.setSpacing(SeparatorSpacingSize.Small),
|
||||
);
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(body),
|
||||
);
|
||||
await message.reply({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [container],
|
||||
});
|
||||
}
|
||||
|
||||
export default async function banListener(client: Client): Promise<void> {
|
||||
client.on(Events.MessageCreate, async (message: Message) => {
|
||||
if (message.author.bot || !message.guild) return;
|
||||
if (!message.content.toLowerCase().startsWith("?ban")) return;
|
||||
|
||||
// Check invoker permission
|
||||
if (!message.member?.permissions.has(PermissionFlagsBits.BanMembers)) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff9900,
|
||||
"⛔ Missing Permissions",
|
||||
"You need the **Ban Members** permission to use this command.",
|
||||
);
|
||||
}
|
||||
|
||||
const args = message.content.trim().split(/\s+/);
|
||||
args.shift(); // drop "?ban"
|
||||
|
||||
const mentionMatch = args[0]?.match(/^<@!?(\d+)>$/);
|
||||
if (!mentionMatch) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0x5865f2,
|
||||
"ℹ️ Usage",
|
||||
"`?ban @member [reason]`",
|
||||
);
|
||||
}
|
||||
|
||||
const targetId = mentionMatch[1];
|
||||
const reason = args.slice(1).join(" ") || "No reason provided";
|
||||
|
||||
const target = await message.guild.members
|
||||
.fetch(targetId)
|
||||
.catch(() => null);
|
||||
if (!target) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff4444,
|
||||
"❌ Member Not Found",
|
||||
"Could not find that member in this server.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!target.bannable) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff4444,
|
||||
"❌ Cannot Ban",
|
||||
"I cannot ban this member. They may have a higher role than me.",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await message.guild.members.ban(targetId, { reason });
|
||||
console.log(
|
||||
`[ban] ${message.author.tag} banned ${target.user.tag} — reason: ${reason}`,
|
||||
);
|
||||
await sendResponse(
|
||||
message,
|
||||
0xff4444,
|
||||
"🔨 Member Banned",
|
||||
`**User:** <@${targetId}> (${target.user.tag})\n**Reason:** ${reason}\n**Banned by:** ${message.author}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[ban] Failed to ban member:", error);
|
||||
await sendResponse(
|
||||
message,
|
||||
0xff4444,
|
||||
"❌ Ban Failed",
|
||||
"An error occurred while trying to ban the member. Check my permissions and try again.",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
110
src/listeners/messageCreate/moderation/kick.ts
Normal file
110
src/listeners/messageCreate/moderation/kick.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
Client,
|
||||
ContainerBuilder,
|
||||
Events,
|
||||
Message,
|
||||
MessageFlags,
|
||||
PermissionFlagsBits,
|
||||
SeparatorBuilder,
|
||||
SeparatorSpacingSize,
|
||||
TextDisplayBuilder,
|
||||
} from "discord.js";
|
||||
|
||||
async function sendResponse(
|
||||
message: Message,
|
||||
color: number,
|
||||
title: string,
|
||||
body: string,
|
||||
): Promise<void> {
|
||||
const container = new ContainerBuilder().setAccentColor(color);
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(`## ${title}`),
|
||||
);
|
||||
container.addSeparatorComponents(
|
||||
new SeparatorBuilder()
|
||||
.setDivider(true)
|
||||
.setSpacing(SeparatorSpacingSize.Small),
|
||||
);
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(body),
|
||||
);
|
||||
await message.reply({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [container],
|
||||
});
|
||||
}
|
||||
|
||||
export default async function kickListener(client: Client): Promise<void> {
|
||||
client.on(Events.MessageCreate, async (message: Message) => {
|
||||
if (message.author.bot || !message.guild) return;
|
||||
if (!message.content.toLowerCase().startsWith("?kick")) return;
|
||||
|
||||
// Check invoker permission
|
||||
if (!message.member?.permissions.has(PermissionFlagsBits.KickMembers)) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff9900,
|
||||
"⛔ Missing Permissions",
|
||||
"You need the **Kick Members** permission to use this command.",
|
||||
);
|
||||
}
|
||||
|
||||
const args = message.content.trim().split(/\s+/);
|
||||
args.shift(); // drop "?kick"
|
||||
|
||||
const mentionMatch = args[0]?.match(/^<@!?(\d+)>$/);
|
||||
if (!mentionMatch) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0x5865f2,
|
||||
"ℹ️ Usage",
|
||||
"`?kick @member [reason]`",
|
||||
);
|
||||
}
|
||||
|
||||
const targetId = mentionMatch[1];
|
||||
const reason = args.slice(1).join(" ") || "No reason provided";
|
||||
|
||||
const target = await message.guild.members
|
||||
.fetch(targetId)
|
||||
.catch(() => null);
|
||||
if (!target) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff4444,
|
||||
"❌ Member Not Found",
|
||||
"Could not find that member in this server.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!target.kickable) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff4444,
|
||||
"❌ Cannot Kick",
|
||||
"I cannot kick this member. They may have a higher role than me.",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await target.kick(reason);
|
||||
console.log(
|
||||
`[kick] ${message.author.tag} kicked ${target.user.tag} — reason: ${reason}`,
|
||||
);
|
||||
await sendResponse(
|
||||
message,
|
||||
0xff8c00,
|
||||
"👢 Member Kicked",
|
||||
`**User:** <@${targetId}> (${target.user.tag})\n**Reason:** ${reason}\n**Kicked by:** ${message.author}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[kick] Failed to kick member:", error);
|
||||
await sendResponse(
|
||||
message,
|
||||
0xff4444,
|
||||
"❌ Kick Failed",
|
||||
"An error occurred while trying to kick the member. Check my permissions and try again.",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
136
src/listeners/messageCreate/moderation/slowdown.ts
Normal file
136
src/listeners/messageCreate/moderation/slowdown.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import {
|
||||
Client,
|
||||
ContainerBuilder,
|
||||
Events,
|
||||
Message,
|
||||
MessageFlags,
|
||||
NewsChannel,
|
||||
PermissionFlagsBits,
|
||||
SeparatorBuilder,
|
||||
SeparatorSpacingSize,
|
||||
TextChannel,
|
||||
TextDisplayBuilder,
|
||||
ThreadChannel,
|
||||
} from "discord.js";
|
||||
|
||||
type SlowmodeChannel = TextChannel | NewsChannel | ThreadChannel;
|
||||
|
||||
async function sendResponse(
|
||||
message: Message,
|
||||
color: number,
|
||||
title: string,
|
||||
body: string,
|
||||
): Promise<void> {
|
||||
const container = new ContainerBuilder().setAccentColor(color);
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(`## ${title}`),
|
||||
);
|
||||
container.addSeparatorComponents(
|
||||
new SeparatorBuilder()
|
||||
.setDivider(true)
|
||||
.setSpacing(SeparatorSpacingSize.Small),
|
||||
);
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(body),
|
||||
);
|
||||
await message.reply({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [container],
|
||||
});
|
||||
}
|
||||
|
||||
// Discord max slowmode: 6 hours (21600 seconds)
|
||||
const MAX_SLOWMODE_SECONDS = 21600;
|
||||
|
||||
export default async function slowdownListener(client: Client): Promise<void> {
|
||||
client.on(Events.MessageCreate, async (message: Message) => {
|
||||
if (message.author.bot || !message.guild) return;
|
||||
if (!message.content.toLowerCase().startsWith("?slowdown")) return;
|
||||
|
||||
// Check invoker permission
|
||||
if (
|
||||
!message.member?.permissions.has(PermissionFlagsBits.ManageChannels)
|
||||
) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff9900,
|
||||
"⛔ Missing Permissions",
|
||||
"You need the **Manage Channels** permission to use this command.",
|
||||
);
|
||||
}
|
||||
|
||||
const args = message.content.trim().split(/\s+/);
|
||||
args.shift(); // drop "?slowdown"
|
||||
|
||||
const seconds = parseInt(args[0] ?? "", 10);
|
||||
|
||||
if (isNaN(seconds) || seconds < 0) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0x5865f2,
|
||||
"ℹ️ Usage",
|
||||
"`?slowdown <seconds>`\n-# Use `0` to disable slowmode. Maximum: 21600 (6 hours).",
|
||||
);
|
||||
}
|
||||
|
||||
if (seconds > MAX_SLOWMODE_SECONDS) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff9900,
|
||||
"⚠️ Duration Too Long",
|
||||
`Discord allows a maximum slowmode of **6 hours** (21600 seconds).\nYou specified **${seconds}s**.`,
|
||||
);
|
||||
}
|
||||
|
||||
const channel = message.channel as SlowmodeChannel;
|
||||
|
||||
if (!("setRateLimitPerUser" in channel)) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff4444,
|
||||
"❌ Unsupported Channel",
|
||||
"Slowmode cannot be set in this type of channel.",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await channel.setRateLimitPerUser(
|
||||
seconds,
|
||||
`Slowmode set by ${message.author.tag}`,
|
||||
);
|
||||
console.log(
|
||||
`[slowdown] ${message.author.tag} set slowmode to ${seconds}s in #${channel.name}`,
|
||||
);
|
||||
|
||||
if (seconds === 0) {
|
||||
await sendResponse(
|
||||
message,
|
||||
0x00cc66,
|
||||
"✅ Slowmode Disabled",
|
||||
`Slowmode has been **disabled** in ${channel}.\n**Changed by:** ${message.author}`,
|
||||
);
|
||||
} else {
|
||||
const readable =
|
||||
seconds < 60
|
||||
? `${seconds} second${seconds !== 1 ? "s" : ""}`
|
||||
: seconds < 3600
|
||||
? `${Math.floor(seconds / 60)} minute${Math.floor(seconds / 60) !== 1 ? "s" : ""}`
|
||||
: `${Math.floor(seconds / 3600)} hour${Math.floor(seconds / 3600) !== 1 ? "s" : ""}`;
|
||||
await sendResponse(
|
||||
message,
|
||||
0x5865f2,
|
||||
"🐢 Slowmode Enabled",
|
||||
`Slowmode set to **${readable}** in ${channel}.\n**Changed by:** ${message.author}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[slowdown] Failed to set slowmode:", error);
|
||||
await sendResponse(
|
||||
message,
|
||||
0xff4444,
|
||||
"❌ Slowmode Failed",
|
||||
"An error occurred while setting slowmode. Check my permissions and try again.",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
147
src/listeners/messageCreate/moderation/timeout.ts
Normal file
147
src/listeners/messageCreate/moderation/timeout.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
Client,
|
||||
ContainerBuilder,
|
||||
Events,
|
||||
Message,
|
||||
MessageFlags,
|
||||
PermissionFlagsBits,
|
||||
SeparatorBuilder,
|
||||
SeparatorSpacingSize,
|
||||
TextDisplayBuilder,
|
||||
} from "discord.js";
|
||||
|
||||
async function sendResponse(
|
||||
message: Message,
|
||||
color: number,
|
||||
title: string,
|
||||
body: string,
|
||||
): Promise<void> {
|
||||
const container = new ContainerBuilder().setAccentColor(color);
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(`## ${title}`),
|
||||
);
|
||||
container.addSeparatorComponents(
|
||||
new SeparatorBuilder()
|
||||
.setDivider(true)
|
||||
.setSpacing(SeparatorSpacingSize.Small),
|
||||
);
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(body),
|
||||
);
|
||||
await message.reply({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [container],
|
||||
});
|
||||
}
|
||||
|
||||
/** Format a duration in seconds into a human-readable string */
|
||||
function formatDuration(seconds: number): string {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = seconds % 60;
|
||||
return `${h}h ${m}m ${s}s`;
|
||||
}
|
||||
|
||||
// Discord timeout maximum: 28 days in seconds
|
||||
const MAX_TIMEOUT_SECONDS = 28 * 24 * 60 * 60;
|
||||
|
||||
export default async function timeoutListener(client: Client): Promise<void> {
|
||||
client.on(Events.MessageCreate, async (message: Message) => {
|
||||
if (message.author.bot || !message.guild) return;
|
||||
if (!message.content.toLowerCase().startsWith("?timeout")) return;
|
||||
|
||||
// Check invoker permission (ModerateMembers = timeout)
|
||||
if (
|
||||
!message.member?.permissions.has(
|
||||
PermissionFlagsBits.ModerateMembers,
|
||||
)
|
||||
) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff9900,
|
||||
"⛔ Missing Permissions",
|
||||
"You need the **Moderate Members** permission to use this command.",
|
||||
);
|
||||
}
|
||||
|
||||
const args = message.content.trim().split(/\s+/);
|
||||
args.shift(); // drop "?timeout"
|
||||
|
||||
const mentionMatch = args[0]?.match(/^<@!?(\d+)>$/);
|
||||
if (!mentionMatch) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0x5865f2,
|
||||
"ℹ️ Usage",
|
||||
"`?timeout @member <seconds>`\n-# Maximum: 28 days (2419200 seconds)",
|
||||
);
|
||||
}
|
||||
|
||||
const targetId = mentionMatch[1];
|
||||
const seconds = parseInt(args[1] ?? "", 10);
|
||||
|
||||
if (isNaN(seconds) || seconds <= 0) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0x5865f2,
|
||||
"ℹ️ Usage",
|
||||
"`?timeout @member <seconds>`\n-# Seconds must be a positive number. Maximum: 2419200 (28 days).",
|
||||
);
|
||||
}
|
||||
|
||||
if (seconds > MAX_TIMEOUT_SECONDS) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff9900,
|
||||
"⚠️ Duration Too Long",
|
||||
`Discord allows a maximum timeout of **28 days** (2419200 seconds).\nYou specified **${seconds}s**.`,
|
||||
);
|
||||
}
|
||||
|
||||
const target = await message.guild.members
|
||||
.fetch(targetId)
|
||||
.catch(() => null);
|
||||
if (!target) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff4444,
|
||||
"❌ Member Not Found",
|
||||
"Could not find that member in this server.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!target.moderatable) {
|
||||
return void sendResponse(
|
||||
message,
|
||||
0xff4444,
|
||||
"❌ Cannot Timeout",
|
||||
"I cannot timeout this member. They may have a higher role than me.",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await target.timeout(seconds * 1000);
|
||||
const readable = formatDuration(seconds);
|
||||
const until = `<t:${Math.floor(Date.now() / 1000) + seconds}:R>`;
|
||||
console.log(
|
||||
`[timeout] ${message.author.tag} timed out ${target.user.tag} for ${readable}`,
|
||||
);
|
||||
await sendResponse(
|
||||
message,
|
||||
0xffa500,
|
||||
"⏱️ Member Timed Out",
|
||||
`**User:** <@${targetId}> (${target.user.tag})\n**Duration:** ${readable}\n**Expires:** ${until}\n**Issued by:** ${message.author}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[timeout] Failed to timeout member:", error);
|
||||
await sendResponse(
|
||||
message,
|
||||
0xff4444,
|
||||
"❌ Timeout Failed",
|
||||
"An error occurred while trying to timeout the member. Check my permissions and try again.",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user