feat: add moderation commands for ban, kick, slowdown, and timeout
All checks were successful
CI / build (push) Successful in 9s

This commit is contained in:
Space-Banane
2026-02-23 10:30:13 +01:00
parent aefa1129cd
commit 829e7247c0
4 changed files with 503 additions and 0 deletions

View 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.",
);
}
});
}

View 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.",
);
}
});
}

View 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.",
);
}
});
}

View 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.",
);
}
});
}