diff --git a/src/commands/ban.ts b/src/commands/ban.ts index 55e038e..8d26467 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -14,7 +14,11 @@ import { } from "discord.js"; import { logModerationAction } from "../modules/logModAction.js"; import { sendSanction } from "../modules/sendSanction.js"; -import { errorReply, successReply } from "../utils/components.js"; +import { + errorReply, + sanctionDmMessage, + successReply, +} from "../utils/components.js"; import { logger } from "../utils/logger.js"; import type { Command } from "../interfaces/command.js"; @@ -103,7 +107,10 @@ const banCommand: Command = { try { await target.send( - `You have been banned from **${interaction.guild?.name ?? "the server"}**.\n**Reason:** ${reason}`, + sanctionDmMessage( + `You have been banned from **${interaction.guild?.name ?? "the server"}**.\n**Reason:** ${reason}`, + "ban", + ), ); } catch { // DMs may be closed; continue without failing the command. @@ -134,6 +141,7 @@ const banCommand: Command = { await logModerationAction(interaction.client, { action: "Member Banned", + colour: "ban", emoji: "๐Ÿ”จ", moderatorTag: interaction.user.username, reason: reason, diff --git a/src/commands/kick.ts b/src/commands/kick.ts index 2f65560..584382f 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -14,7 +14,11 @@ import { } from "discord.js"; import { logModerationAction } from "../modules/logModAction.js"; import { sendSanction } from "../modules/sendSanction.js"; -import { errorReply, successReply } from "../utils/components.js"; +import { + errorReply, + sanctionDmMessage, + successReply, +} from "../utils/components.js"; import { logger } from "../utils/logger.js"; import type { Command } from "../interfaces/command.js"; @@ -98,7 +102,10 @@ const kickCommand: Command = { try { await target.send( - `You have been kicked from **${interaction.guild?.name ?? "the server"}**.\n**Reason:** ${reason}`, + sanctionDmMessage( + `You have been kicked from **${interaction.guild?.name ?? "the server"}**.\n**Reason:** ${reason}`, + "kick", + ), ); } catch { // DMs may be closed; continue without failing the command. @@ -125,6 +132,7 @@ const kickCommand: Command = { await logModerationAction(interaction.client, { action: "Member Kicked", + colour: "kick", emoji: "๐Ÿ‘ข", moderatorTag: interaction.user.username, reason: reason, diff --git a/src/commands/mute.ts b/src/commands/mute.ts index 4e78a3b..e8bf4a7 100644 --- a/src/commands/mute.ts +++ b/src/commands/mute.ts @@ -14,7 +14,11 @@ import { } from "discord.js"; import { logModerationAction } from "../modules/logModAction.js"; import { sendSanction } from "../modules/sendSanction.js"; -import { errorReply, successReply } from "../utils/components.js"; +import { + errorReply, + sanctionDmMessage, + successReply, +} from "../utils/components.js"; import { logger } from "../utils/logger.js"; import type { Command } from "../interfaces/command.js"; @@ -155,7 +159,10 @@ const muteCommand: Command = { try { await target.send( - `You have been muted in **${interaction.guild?.name ?? "the server"}** for **${durationLabel}**.\n**Reason:** ${reason}`, + sanctionDmMessage( + `You have been muted in **${interaction.guild?.name ?? "the server"}** for **${durationLabel}**.\n**Reason:** ${reason}`, + "mute", + ), ); } catch { // DMs may be closed; continue without failing the command. @@ -163,6 +170,7 @@ const muteCommand: Command = { await logModerationAction(interaction.client, { action: "Member Muted", + colour: "mute", emoji: "๐Ÿ”‡", moderatorTag: interaction.user.username, reason: reason, diff --git a/src/commands/prune.ts b/src/commands/prune.ts index 506fbfd..1dd30f4 100644 --- a/src/commands/prune.ts +++ b/src/commands/prune.ts @@ -102,6 +102,7 @@ const pruneCommand: Command = { await logModerationAction(interaction.client, { action: "Messages Pruned", + colour: "prune", emoji: "๐Ÿ—‘๏ธ", moderatorTag: interaction.user.username, reason: `Bulk delete of ${deletedCount.toString()} messages in <#${rawChannel.id}>`, diff --git a/src/commands/softban.ts b/src/commands/softban.ts index bd86836..990cf50 100644 --- a/src/commands/softban.ts +++ b/src/commands/softban.ts @@ -14,7 +14,11 @@ import { } from "discord.js"; import { logModerationAction } from "../modules/logModAction.js"; import { sendSanction } from "../modules/sendSanction.js"; -import { errorReply, successReply } from "../utils/components.js"; +import { + errorReply, + sanctionDmMessage, + successReply, +} from "../utils/components.js"; import { logger } from "../utils/logger.js"; import type { Command } from "../interfaces/command.js"; @@ -94,7 +98,10 @@ const softbanCommand: Command = { try { await target.send( - `You have been softbanned from **${interaction.guild?.name ?? "the server"}** (your recent messages have been removed).\n**Reason:** ${reason}`, + sanctionDmMessage( + `You have been softbanned from **${interaction.guild?.name ?? "the server"}** (your recent messages have been removed).\n**Reason:** ${reason}`, + "softban", + ), ); } catch { // DMs may be closed; continue without failing the command. @@ -130,6 +137,7 @@ const softbanCommand: Command = { await logModerationAction(interaction.client, { action: "Member Softbanned", + colour: "softban", emoji: "๐Ÿงน", moderatorTag: interaction.user.username, reason: reason, diff --git a/src/commands/unban.ts b/src/commands/unban.ts index 790d61c..53f8ac2 100644 --- a/src/commands/unban.ts +++ b/src/commands/unban.ts @@ -84,6 +84,7 @@ const unbanCommand: Command = { await logModerationAction(interaction.client, { action: "Member Unbanned", + colour: "unban", emoji: "๐Ÿ”“", moderatorTag: interaction.user.username, reason: reason, diff --git a/src/commands/unmute.ts b/src/commands/unmute.ts index 3bd2ae2..b1251f7 100644 --- a/src/commands/unmute.ts +++ b/src/commands/unmute.ts @@ -103,6 +103,7 @@ const unmuteCommand: Command = { await logModerationAction(interaction.client, { action: "Member Unmuted", + colour: "unmute", emoji: "๐Ÿ”Š", moderatorTag: interaction.user.username, reason: reason, diff --git a/src/commands/warn.ts b/src/commands/warn.ts index 5aafd87..523aabf 100644 --- a/src/commands/warn.ts +++ b/src/commands/warn.ts @@ -14,7 +14,11 @@ import { } from "discord.js"; import { logModerationAction } from "../modules/logModAction.js"; import { sendSanction } from "../modules/sendSanction.js"; -import { errorReply, successReply } from "../utils/components.js"; +import { + errorReply, + sanctionDmMessage, + successReply, +} from "../utils/components.js"; import { logger } from "../utils/logger.js"; import type { Command } from "../interfaces/command.js"; @@ -99,7 +103,10 @@ const warnCommand: Command = { try { await target.send( - `You have received a warning in **${interaction.guild?.name ?? "the server"}**.\n**Reason:** ${reason}`, + sanctionDmMessage( + `You have received a warning in **${interaction.guild?.name ?? "the server"}**.\n**Reason:** ${reason}`, + "warn", + ), ); } catch { // DMs may be closed; continue without failing the command. @@ -107,6 +114,7 @@ const warnCommand: Command = { await logModerationAction(interaction.client, { action: "Warning Issued", + colour: "warn", emoji: "โš ๏ธ", moderatorTag: interaction.user.username, reason: reason, diff --git a/src/events/guildAuditLogEntryCreate.ts b/src/events/guildAuditLogEntryCreate.ts index 462deef..75feaae 100644 --- a/src/events/guildAuditLogEntryCreate.ts +++ b/src/events/guildAuditLogEntryCreate.ts @@ -57,6 +57,7 @@ const handleManualUnban = async( await logModerationAction(client, { action: "Member Unbanned", + colour: "unban", emoji: "๐Ÿ”“", moderatorTag: moderatorTag, reason: resolvedReason, @@ -98,6 +99,7 @@ const handleManualBan = async( await logModerationAction(client, { action: "Member Banned", + colour: "ban", emoji: "๐Ÿ”จ", moderatorTag: moderatorTag, reason: resolvedReason, @@ -139,6 +141,7 @@ const handleManualKick = async( await logModerationAction(client, { action: "Member Kicked", + colour: "kick", emoji: "๐Ÿ‘ข", moderatorTag: moderatorTag, reason: resolvedReason, @@ -191,6 +194,7 @@ const handleManualTimeout = async( await logModerationAction(client, { action: "Member Muted", + colour: "mute", emoji: "๐Ÿ”‡", moderatorTag: moderatorTag, reason: resolvedReason, diff --git a/src/events/guildMemberUpdate.ts b/src/events/guildMemberUpdate.ts index a392964..e48116b 100644 --- a/src/events/guildMemberUpdate.ts +++ b/src/events/guildMemberUpdate.ts @@ -41,6 +41,7 @@ export const onGuildMemberUpdate = async( await logActivity({ client: newMember.client, + colour: "nicknameChange", emoji: "๐Ÿ“", fields: [ `**User**: ${username} (\`${userId}\`)`, @@ -64,6 +65,7 @@ export const onGuildMemberUpdate = async( await logActivity({ client: newMember.client, + colour: "rolesAdded", emoji: "โž•", fields: [ `**User**: ${username} (\`${userId}\`)`, @@ -86,6 +88,7 @@ export const onGuildMemberUpdate = async( await logActivity({ client: newMember.client, + colour: "rolesRemoved", emoji: "โž–", fields: [ `**User**: ${username} (\`${userId}\`)`, @@ -98,6 +101,7 @@ export const onGuildMemberUpdate = async( if (oldMember.avatar !== newMember.avatar) { await logActivity({ client: newMember.client, + colour: "serverAvatar", emoji: "๐Ÿ–ผ๏ธ", fields: [ `**User**: ${username} (\`${userId}\`)`, diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index 2cd679d..aac3f34 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -54,6 +54,7 @@ export const onMessageDelete = async( await logActivity({ client: message.client, + colour: "messageDeleted", emoji: "๐Ÿ—‘๏ธ", fields: fields, title: "Message Deleted", diff --git a/src/events/messageUpdate.ts b/src/events/messageUpdate.ts index 9617cc7..a8f9175 100644 --- a/src/events/messageUpdate.ts +++ b/src/events/messageUpdate.ts @@ -53,6 +53,7 @@ export const onMessageUpdate = async( await logActivity({ client: newMessage.client, + colour: "messageEdited", emoji: "โœ๏ธ", fields: fields, title: "Message Edited", diff --git a/src/events/threadCreate.ts b/src/events/threadCreate.ts index 82a699a..70c98b4 100644 --- a/src/events/threadCreate.ts +++ b/src/events/threadCreate.ts @@ -27,6 +27,7 @@ export const onThreadCreate = async( await logActivity({ client: thread.client, + colour: "threadCreated", emoji: "๐Ÿงต", fields: fields, title: "Thread Created", diff --git a/src/events/threadDelete.ts b/src/events/threadDelete.ts index ef2ecb0..306ead1 100644 --- a/src/events/threadDelete.ts +++ b/src/events/threadDelete.ts @@ -24,6 +24,7 @@ export const onThreadDelete = async( await logActivity({ client: thread.client, + colour: "threadDeleted", emoji: "๐Ÿ—‘๏ธ", fields: fields, title: "Thread Deleted", diff --git a/src/events/threadUpdate.ts b/src/events/threadUpdate.ts index 274b63f..f01967f 100644 --- a/src/events/threadUpdate.ts +++ b/src/events/threadUpdate.ts @@ -52,6 +52,7 @@ export const onThreadUpdate = async( await logActivity({ client: newThread.client, + colour: "threadUpdated", emoji: "๐Ÿ”„", fields: fields, title: "Thread Updated", diff --git a/src/events/userUpdate.ts b/src/events/userUpdate.ts index 9f0ea73..adb7185 100644 --- a/src/events/userUpdate.ts +++ b/src/events/userUpdate.ts @@ -29,6 +29,7 @@ export const onUserUpdate = async( if (oldUser.username !== newUser.username) { await logActivity({ client: newUser.client, + colour: "usernameChanged", emoji: "โœ๏ธ", fields: [ `**User**: ${newUser.username} (\`${newUser.id}\`)`, @@ -42,6 +43,7 @@ export const onUserUpdate = async( if (oldUser.globalName !== newUser.globalName) { await logActivity({ client: newUser.client, + colour: "displayName", emoji: "๐Ÿ“›", fields: [ `**User**: ${newUser.username} (\`${newUser.id}\`)`, @@ -55,6 +57,7 @@ export const onUserUpdate = async( if (oldUser.avatar !== newUser.avatar) { await logActivity({ client: newUser.client, + colour: "avatarChanged", emoji: "๐Ÿ–ผ๏ธ", fields: [ `**User**: ${newUser.username} (\`${newUser.id}\`)`, diff --git a/src/events/voiceStateUpdate.ts b/src/events/voiceStateUpdate.ts index 05a3a95..e143f8d 100644 --- a/src/events/voiceStateUpdate.ts +++ b/src/events/voiceStateUpdate.ts @@ -60,6 +60,7 @@ export const onVoiceStateUpdate = async( await logActivity({ client: user.client, + colour: "voiceState", emoji: "๐Ÿ”Š", fields: fields, title: "Voice State Update", diff --git a/src/modules/logActivity.ts b/src/modules/logActivity.ts index da6d618..d08a960 100644 --- a/src/modules/logActivity.ts +++ b/src/modules/logActivity.ts @@ -6,11 +6,12 @@ import { ChannelType, type Client } from "discord.js"; import { channelConfig } from "../config/channels.js"; -import { activityMessage } from "../utils/components.js"; +import { activityMessage, type ColourKey } from "../utils/components.js"; import { logger } from "../utils/logger.js"; interface ActivityData { readonly client: Client; + readonly colour: ColourKey; readonly emoji: string; readonly fields: string; readonly title: string; @@ -35,7 +36,9 @@ const logActivity = async(data: ActivityData): Promise => { return; } - await rawChannel.send(activityMessage(data.emoji, data.title, data.fields)); + await rawChannel.send( + activityMessage(data.emoji, data.title, data.fields, data.colour), + ); } catch (error) { await logger.error( "Failed to log activity", diff --git a/src/modules/logModAction.ts b/src/modules/logModAction.ts index a7a9b11..32fe65d 100644 --- a/src/modules/logModAction.ts +++ b/src/modules/logModAction.ts @@ -7,11 +7,12 @@ import { ChannelType, type Client } from "discord.js"; import { channelConfig } from "../config/channels.js"; -import { modLogMessage } from "../utils/components.js"; +import { modLogMessage, type ColourKey } from "../utils/components.js"; import { logger } from "../utils/logger.js"; interface ModuleActionData { readonly action: string; + readonly colour: ColourKey; readonly emoji: string; readonly moderatorTag: string; readonly reason: string; @@ -57,7 +58,7 @@ const logModerationAction = async( join("\n"); await rawChannel.send( - modLogMessage(`${data.emoji} ${data.action}`, fields, data.source), + modLogMessage(`${data.emoji} ${data.action}`, fields, data.source, data.colour), ); } catch (error) { await logger.error( diff --git a/src/utils/components.ts b/src/utils/components.ts index 15efeb9..4aa5708 100644 --- a/src/utils/components.ts +++ b/src/utils/components.ts @@ -27,13 +27,33 @@ const MessageFlags = { } as const; const Colours = { - error: 0xED_42_45, - info: 0x58_65_F2, - join: 0x57_F2_87, - leave: 0x99_AA_B5, - modAction: 0xE6_7E_22, - success: 0x57_F2_87, - warning: 0xFE_E7_5C, + avatarChanged: 0xEC_40_7A, + ban: 0xE7_4C_3C, + displayName: 0xAB_47_BC, + error: 0xED_42_45, + info: 0x58_65_F2, + join: 0x57_F2_87, + kick: 0xE6_7E_22, + leave: 0x99_AA_B5, + messageDeleted: 0xC0_39_2B, + messageEdited: 0x29_80_B9, + mute: 0xF3_9C_12, + nicknameChange: 0x9B_59_B6, + prune: 0x34_98_DB, + rolesAdded: 0x27_AE_60, + rolesRemoved: 0xE7_4C_3C, + serverAvatar: 0x16_A0_85, + softban: 0xD3_54_00, + success: 0x57_F2_87, + threadCreated: 0x00_BC_D4, + threadDeleted: 0x54_6E_7A, + threadUpdated: 0x5D_AD_E2, + unban: 0x2E_CC_71, + unmute: 0x1A_BC_9C, + usernameChanged: 0x7E_57_C2, + voiceState: 0x43_B5_81, + warn: 0xF1_C4_0F, + warning: 0xFE_E7_5C, } as const; /* eslint-enable @typescript-eslint/naming-convention -- Enum-style constants use PascalCase by convention */ @@ -106,18 +126,21 @@ const errorReply = (title: string, body: string): Record => { * @param title - The formatted title string (emoji + action combined). * @param fields - The formatted field lines for the log entry. * @param source - Whether the action originated from a command or the audit log. + * @param colour - The accent colour for the container. * @returns A Discord message payload object. */ const modLogMessage = ( title: string, fields: string, source: "Command" | "Audit Log", + colour: ColourKey, +// eslint-disable-next-line @typescript-eslint/max-params -- Four params is the minimum needed for this helper ): Record => { return { allowedMentions: { parse: [] }, components: [ buildContainer( - "modAction", + colour, `## ${title}`, fields, `*Source: ${source}*`, @@ -132,16 +155,19 @@ const modLogMessage = ( * @param emoji - The emoji to prefix the title with. * @param title - The title of the activity entry. * @param fields - The formatted field lines. + * @param colour - The accent colour for the container. * @returns A Discord message payload object. */ const activityMessage = ( emoji: string, title: string, fields: string, + colour: ColourKey, +// eslint-disable-next-line @typescript-eslint/max-params -- Four params is the minimum needed for this helper ): Record => { return { allowedMentions: { parse: [] }, - components: [ buildContainer("info", `## ${emoji} ${title}`, fields) ], + components: [ buildContainer(colour, `## ${emoji} ${title}`, fields) ], flags: MessageFlags.IsComponentsV2, }; }; @@ -173,10 +199,37 @@ const memberMessage = ( }; }; +const sanctionLinks = [ + // eslint-disable-next-line stylistic/max-len -- URL cannot be shortened + "**Appeal this sanction:** https://forms.nhcarrigan.com/o/docs/forms/4w5VHsYiEkiS2mewvtuJYL/4", + "**View sanction logs:** https://hikari.nhcarrigan.com/sanctions", + "**Contact us:** https://docs.nhcarrigan.com/about/contact/", + "**Rejoin our community:** https://chat.nhcarrigan.com", +].join("\n"); + +/** + * Builds a Components v2 DM payload for sanction notifications. + * Includes a separator between the sanction details and the resource links. + * @param sanctionText - The formatted sanction message (action + reason). + * @param colour - The accent colour for the container. + * @returns A Discord message payload object. + */ +const sanctionDmMessage = ( + sanctionText: string, + colour: ColourKey, +): Record => { + return { + components: [ buildContainer(colour, sanctionText, sanctionLinks) ], + flags: MessageFlags.IsComponentsV2, + }; +}; + +export type { ColourKey }; export { activityMessage, errorReply, memberMessage, modLogMessage, + sanctionDmMessage, successReply, };