diff --git a/package.json b/package.json index fa50701..a5d0a92 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build:ci": "rm -rf dist && NODE_ENV=production tsc --outDir dist", "start:ci": "NODE_ENV=production node dist/index.js ", "dev": "NODE_ENV=development tsx --watch src/index.ts", - "test": "tsx --test src/**/*.test.ts tests/*.test.ts", + "test": "tsx --test 'src/**/*.test.ts' 'tests/*.test.ts'", "lint": "biome lint .", "lint:fix": "biome lint --fix .", "format": "biome format --write .", diff --git a/src/events/spam-detection/logs.test.ts b/src/events/spam-detection/logs.test.ts new file mode 100644 index 0000000..bcb8fb6 --- /dev/null +++ b/src/events/spam-detection/logs.test.ts @@ -0,0 +1,35 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import type { Message } from "discord.js"; +import { HOUR } from "../../constants/time.js"; +import { createLogTextContent, type LogFunctionOptions } from "./logs.js"; +import type { ContentBasedRule } from "./rules-config.js"; + +describe("spam-detection/logs -> createLogTextContent", () => { + it("should create log content for a content-based rule", () => { + // Mock options for a content-based rule + const options = { + rule: { + type: "contentBased", + isBrokenBy: () => true, + action: async () => {}, + }, + messages: [ + { content: "This message contains a banned tag", channelId: "123", author: { id: "1" } }, + ] as Message[], + deletedMessagesCount: 1, + reason: "Contains banned tag", + muteDuration: 1 * HOUR, + } satisfies LogFunctionOptions; + + const logContent = createLogTextContent(options); + console.log(logContent); + + // Basic assertions to check if the log content includes expected information + assert(logContent.includes("**Rule Broken:** Contains banned tag")); + assert(logContent.includes("**User:** <@1>")); + assert(logContent.includes("**Flagged Message:**")); + assert(logContent.includes("This message contains a banned tag")); + assert(logContent.includes("**Channel:** <#123>")); + }); +}); diff --git a/src/events/spam-detection/logs.ts b/src/events/spam-detection/logs.ts index cd9f10c..2bebcb5 100644 --- a/src/events/spam-detection/logs.ts +++ b/src/events/spam-detection/logs.ts @@ -28,72 +28,95 @@ export type LogFunctionOptions = { export type LogFunction = (options: LogFunctionOptions) => Promise; export const createLogTextContent = (options: LogFunctionOptions) => { - let contentString = ""; + const content: string[] = []; - contentString += makeLogMessageTitleAndContent("Rule Broken", `${options.reason}\n`); - contentString += makeLogMessageTitleAndContent("User", `<@${options.messages[0].author.id}>\n`); + content.push(makeLogMessageTitleAndContent("Rule Broken", `${options.reason}\n`)); + content.push(makeLogMessageTitleAndContent("User", `<@${options.messages[0].author.id}>\n`)); switch (options.rule.type) { case "contentBased": { const flaggedMessage = options.messages[0]; - contentString += makeLogMessageTitleAndContent( - "Flagged Message", - `\`\n\n${flaggedMessage.content}\`\n` + content.push( + makeLogMessageTitleAndContent("Flagged Message", `\`\n\n${flaggedMessage.content}\`\n`) ); - contentString += SPACER; - contentString += makeLogMessageTitleAndContent("Channel", `<#${flaggedMessage.channelId}>`); + content.push(SPACER); + content.push(makeLogMessageTitleAndContent("Channel", `<#${flaggedMessage.channelId}>`)); break; } case "crossChannel": { if (options.rule.isBrokenBy.name === "isCrossPost") { - contentString += `Posted in **${options.rule.channelCount}** channels within **${timeToString(options.rule.timeframe)} **\n`; + content.push( + `Posted in **${options.rule.channelCount}** channels within **${timeToString(options.rule.timeframe)} **\n` + ); const flaggedMessage = options.messages[0]; const affectedChannels = new Set(options.messages.map((message) => message.channelId)); - contentString += makeLogMessageTitleAndContent( - "Flagged Message", - `\n\n${flaggedMessage.content}\n` - ); - contentString += SPACER; - contentString += makeLogMessageTitleAndContent( - "Channels Involved", - Array.from(affectedChannels) - .map((id) => `<#${id}>`) - .join(", ") + const hasMessage = flaggedMessage.content && flaggedMessage.content.trim().length > 0; + const hasAttachments = flaggedMessage.attachments.size > 0; + if (hasMessage) { + content.push( + makeLogMessageTitleAndContent("Flagged Message", `\n\n${flaggedMessage.content}\n`) + ); + } + if (hasAttachments) { + content.push( + makeLogMessageTitleAndContent( + "Flagged Message", + `\n\n[Attachment: ${flaggedMessage.attachments.first()?.name}]\n` + ) + ); + } + if (!hasMessage && !hasAttachments) { + content.push(makeLogMessageTitleAndContent("Flagged Message", `\n\n[No Text Content]\n`)); + } + content.push(SPACER); + content.push( + makeLogMessageTitleAndContent( + "Channels Involved", + Array.from(affectedChannels) + .map((id) => `<#${id}>`) + .join(", ") + ) ); } break; } case "frequencyBased": { - contentString += `Sent **${options.rule.frequency}** messages within **${timeToString(options.rule.timeframe)}**\n`; + content.push( + `Sent **${options.rule.frequency}** messages within **${timeToString(options.rule.timeframe)}**\n` + ); const displayedMessages = options.messages.slice(0, 5); const displayedCount = displayedMessages.length; const remainingCount = options.messages.length - displayedCount; - contentString += `**Messages Involved:**\n`; - contentString += displayedMessages - .map((message) => { - const contentPreview = - message.content.length > 50 ? `${message.content.slice(0, 47)}...` : message.content; - return `- ${contentPreview}`; - }) - .join("\n"); + content.push(`**Messages Involved:**\n`); + content.push( + displayedMessages + .map((message) => { + const contentPreview = + message.content.length > 50 ? `${message.content.slice(0, 47)}...` : message.content; + return `- ${contentPreview}`; + }) + .join("\n") + ); if (remainingCount > 0) { - contentString += `\n ...and ${remainingCount} more\n`; + content.push(`\n ...and ${remainingCount} more\n`); } break; } } - contentString += SPACER; - contentString += "**Action(s) Taken:**\n"; + content.push(SPACER); + content.push("**Action(s) Taken:**\n"); if (options.deletedMessagesCount > 0) { - contentString += `- Deleted ${options.deletedMessagesCount} message${options.deletedMessagesCount > 1 ? "s" : ""}\n`; + content.push( + `- Deleted ${options.deletedMessagesCount} message${options.deletedMessagesCount > 1 ? "s" : ""}\n` + ); } if (options.muteDuration) { - contentString += `- Muted user for ${timeToString(options.muteDuration)}\n`; + content.push(`- Muted user for ${timeToString(options.muteDuration)}\n`); } - return contentString; + return content.join(""); }; export const defaultLogFunction: LogFunction = async (options) => {