Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand Down
35 changes: 35 additions & 0 deletions src/events/spam-detection/logs.test.ts
Original file line number Diff line number Diff line change
@@ -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<ContentBasedRule>;

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>"));
});
});
91 changes: 57 additions & 34 deletions src/events/spam-detection/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,72 +28,95 @@ export type LogFunctionOptions<T = Rule> = {
export type LogFunction<T = Rule> = (options: LogFunctionOptions<T>) => Promise<void>;

export const createLogTextContent = <T extends Rule>(options: LogFunctionOptions<T>) => {
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) => {
Expand Down
Loading