feat: rewrite as moderation bot (Keiko)
Node.js CI / CI (pull_request) Failing after 15s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 54s

Replaces the old AI companion bot with a full Discord moderation system.

Commands: warn, mute, unmute, kick, softban, ban, unban, prune
Logging: member join/leave, activity (messages/threads/voice), mod actions
Audit log: captures manual bans, kicks, timeouts, and unbans
Sanctions: posts to Hikari sanction API for all applicable actions

All commands are ephemeral and use Components v2. Permission and role
hierarchy checks are enforced on every applicable command.
This commit is contained in:
2026-03-24 20:15:41 -07:00
committed by Naomi Carrigan
parent d44be4880e
commit 76e559876b
52 changed files with 2866 additions and 1446 deletions
+157
View File
@@ -0,0 +1,157 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable max-lines-per-function -- Command handlers have many validation and action steps */
/* eslint-disable max-statements -- Command handlers have many validation and action steps */
/* eslint-disable complexity -- Command handlers have multiple validation branches */
import {
InteractionContextType,
PermissionFlagsBits,
SlashCommandBuilder,
} from "discord.js";
import { logModerationAction } from "../modules/logModAction.js";
import { sendSanction } from "../modules/sendSanction.js";
import { errorReply, successReply } from "../utils/components.js";
import { logger } from "../utils/logger.js";
import type { Command } from "../interfaces/command.js";
const banCommand: Command = {
data: new SlashCommandBuilder().
setName("ban").
setDescription("Ban a member from the server.").
setDefaultMemberPermissions(PermissionFlagsBits.BanMembers).
setContexts([ InteractionContextType.Guild ]).
addUserOption((option) => {
return option.
setName("user").
setDescription("The member to ban.").
setRequired(true);
}).
addStringOption((option) => {
return option.
setName("reason").
setDescription("The reason for the ban.").
setRequired(true).
setMaxLength(512);
}).
addIntegerOption((option) => {
return option.
setName("days").
setDescription(
"Days of message history to delete (07). Defaults to 0.",
).
setRequired(false).
setMinValue(0).
setMaxValue(7);
}),
execute: async(interaction) => {
await interaction.deferReply({ ephemeral: true });
const hasPermission = interaction.memberPermissions?.has(
PermissionFlagsBits.BanMembers,
);
if (hasPermission !== true) {
await interaction.editReply(
errorReply(
"Insufficient Permissions",
"You do not have permission to use this command.",
),
);
return;
}
const target = interaction.options.getUser("user", true);
const reason = interaction.options.getString("reason", true);
const days = interaction.options.getInteger("days") ?? 0;
if (target.bot) {
await interaction.editReply(
errorReply("Invalid Target", "You cannot ban a bot."),
);
return;
}
if (target.id === interaction.user.id) {
await interaction.editReply(
errorReply("Invalid Target", "You cannot ban yourself."),
);
return;
}
const member = interaction.guild?.members.cache.get(target.id);
if (member) {
const selfMember = interaction.guild?.members.cache.get(
interaction.user.id,
);
if (
selfMember
&& member.roles.highest.position >= selfMember.roles.highest.position
) {
await interaction.editReply(
errorReply(
"Insufficient Permissions",
"You cannot ban someone with an equal or higher role.",
),
);
return;
}
try {
await target.send(
`You have been banned from **${interaction.guild?.name ?? "the server"}**.\n**Reason:** ${reason}`,
);
} catch {
// DMs may be closed; continue without failing the command.
}
}
try {
await interaction.guild?.bans.create(target.id, {
deleteMessageSeconds: days * 86_400,
reason: reason,
});
} catch {
await interaction.editReply(
errorReply(
"Action Failed",
"Failed to ban the member. Check my permissions and role hierarchy.",
),
);
return;
}
const sanctionNumber = await sendSanction({
reason: reason,
type: "ban",
username: target.username,
uuid: target.id,
});
await logModerationAction(interaction.client, {
action: "Member Banned",
emoji: "🔨",
moderatorTag: interaction.user.username,
reason: reason,
sanctionNumber: sanctionNumber,
source: "Command",
targetId: target.id,
targetTag: target.username,
});
await interaction.editReply(
successReply(
"Member Banned",
`**User**: ${target.username} (\`${target.id}\`)\n**Days Deleted**: ${days.toString()}\n**Reason**: ${reason}`,
),
);
},
};
void logger.log("debug", "Ban command loaded.");
export { banCommand };