Files
keiko/src/commands/prune.ts
T
hikari 453ebd0f15
Node.js CI / CI (push) Successful in 28s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 2m12s
feat: sanction DM links and per-event colour coding (#13)
## Summary

- Adds resource links (appeal form, sanction logs, contact page, community invite) to all sanction DMs, separated from the sanction text by a Components v2 separator
- Adds a unique accent colour for every mod log and activity log event type, giving each action a distinct visual identity at a glance

## Changes

- `src/utils/components.ts` — Added `sanctionDmMessage` helper with two-section container (sanction text + links); added full `Colours` palette covering all sanction and activity event types; added `ColourKey` export
- `src/commands/{ban,kick,mute,softban,warn}.ts` — Updated DMs to use `sanctionDmMessage` with the appropriate colour
- `src/modules/logModAction.ts` / `logActivity.ts` — Thread `colour` parameter through to message builders
- All event and command files updated with their respective colours

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #13
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-03-31 17:33:35 -07:00

127 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable max-lines-per-function -- Command handlers have many validation and action steps */
import {
InteractionContextType,
PermissionFlagsBits,
SlashCommandBuilder,
type GuildTextBasedChannel,
} from "discord.js";
import { logModerationAction } from "../modules/logModAction.js";
import { errorReply, successReply } from "../utils/components.js";
import { logger } from "../utils/logger.js";
import type { Command } from "../interfaces/command.js";
/**
* Performs a bulk delete on a text channel and returns the number deleted,
* or null if the bulk delete fails.
* Wrapped to handle the type narrowing gap after isTextBased() checks.
* @param channel - The text channel to delete messages from.
* @param amount - The number of messages to delete.
* @returns The number of messages actually deleted, or null on failure.
*/
const bulkDeleteMessages = async(
channel: GuildTextBasedChannel,
amount: number,
): Promise<number | null> => {
try {
const deleted = await channel.bulkDelete(amount, true);
return deleted.size;
} catch {
return null;
}
};
const pruneCommand: Command = {
data: new SlashCommandBuilder().
setName("prune").
setDescription("Delete the last N messages in this channel.").
setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages).
setContexts([ InteractionContextType.Guild ]).
addIntegerOption((option) => {
return option.
setName("amount").
setDescription("Number of messages to delete (1100).").
setRequired(true).
setMinValue(1).
setMaxValue(100);
}),
execute: async(interaction) => {
await interaction.deferReply({ ephemeral: true });
const hasPermission = interaction.memberPermissions?.has(
PermissionFlagsBits.ManageMessages,
);
if (hasPermission !== true) {
await interaction.editReply(
errorReply(
"Insufficient Permissions",
"You do not have permission to use this command.",
),
);
return;
}
const amount = interaction.options.getInteger("amount", true);
const rawChannel = interaction.channel;
if (rawChannel?.isTextBased() !== true) {
await interaction.editReply(
errorReply(
"Invalid Channel",
"This command can only be used in text channels.",
),
);
return;
}
const channelName = "name" in rawChannel
? String(rawChannel.name)
: rawChannel.id;
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- rawChannel is narrowed by isTextBased() but the type union is not fully resolved */
const textChannel = rawChannel as GuildTextBasedChannel;
const deletedCount = await bulkDeleteMessages(textChannel, amount);
if (deletedCount === null) {
await interaction.editReply(
errorReply(
"Action Failed",
"Failed to delete messages. "
+ "Messages older than 14 days cannot be bulk deleted.",
),
);
return;
}
await logModerationAction(interaction.client, {
action: "Messages Pruned",
colour: "prune",
emoji: "🗑️",
moderatorTag: interaction.user.username,
reason: `Bulk delete of ${deletedCount.toString()} messages in <#${rawChannel.id}>`,
sanctionNumber: null,
source: "Command",
targetId: rawChannel.id,
targetTag: `#${channelName}`,
});
await interaction.editReply(
successReply(
"Messages Pruned",
`Deleted **${deletedCount.toString()}** messages from <#${rawChannel.id}>.`,
),
);
},
};
void logger.log("debug", "Prune command loaded.");
export { pruneCommand };