Skip to content

Commit aa953d7

Browse files
authored
Feat: improve spam detection logging (#16)
* fix: correct test script quotes in package.json * 🔨 refactor: use an array to build the content string for performance * 🚨 test: add tests for createLogTextContent in spam detection logs * 🌟 feat: include text and attachment details in spam-detection logging
1 parent ef5cacf commit aa953d7

3 files changed

Lines changed: 93 additions & 35 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"build:ci": "rm -rf dist && NODE_ENV=production tsc --outDir dist",
99
"start:ci": "NODE_ENV=production node dist/index.js ",
1010
"dev": "NODE_ENV=development tsx --watch src/index.ts",
11-
"test": "tsx --test src/**/*.test.ts tests/*.test.ts",
11+
"test": "tsx --test 'src/**/*.test.ts' 'tests/*.test.ts'",
1212
"lint": "biome lint .",
1313
"lint:fix": "biome lint --fix .",
1414
"format": "biome format --write .",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import assert from "node:assert";
2+
import { describe, it } from "node:test";
3+
import type { Message } from "discord.js";
4+
import { HOUR } from "../../constants/time.js";
5+
import { createLogTextContent, type LogFunctionOptions } from "./logs.js";
6+
import type { ContentBasedRule } from "./rules-config.js";
7+
8+
describe("spam-detection/logs -> createLogTextContent", () => {
9+
it("should create log content for a content-based rule", () => {
10+
// Mock options for a content-based rule
11+
const options = {
12+
rule: {
13+
type: "contentBased",
14+
isBrokenBy: () => true,
15+
action: async () => {},
16+
},
17+
messages: [
18+
{ content: "This message contains a banned tag", channelId: "123", author: { id: "1" } },
19+
] as Message[],
20+
deletedMessagesCount: 1,
21+
reason: "Contains banned tag",
22+
muteDuration: 1 * HOUR,
23+
} satisfies LogFunctionOptions<ContentBasedRule>;
24+
25+
const logContent = createLogTextContent(options);
26+
console.log(logContent);
27+
28+
// Basic assertions to check if the log content includes expected information
29+
assert(logContent.includes("**Rule Broken:** Contains banned tag"));
30+
assert(logContent.includes("**User:** <@1>"));
31+
assert(logContent.includes("**Flagged Message:**"));
32+
assert(logContent.includes("This message contains a banned tag"));
33+
assert(logContent.includes("**Channel:** <#123>"));
34+
});
35+
});

src/events/spam-detection/logs.ts

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -28,72 +28,95 @@ export type LogFunctionOptions<T = Rule> = {
2828
export type LogFunction<T = Rule> = (options: LogFunctionOptions<T>) => Promise<void>;
2929

3030
export const createLogTextContent = <T extends Rule>(options: LogFunctionOptions<T>) => {
31-
let contentString = "";
31+
const content: string[] = [];
3232

33-
contentString += makeLogMessageTitleAndContent("Rule Broken", `${options.reason}\n`);
34-
contentString += makeLogMessageTitleAndContent("User", `<@${options.messages[0].author.id}>\n`);
33+
content.push(makeLogMessageTitleAndContent("Rule Broken", `${options.reason}\n`));
34+
content.push(makeLogMessageTitleAndContent("User", `<@${options.messages[0].author.id}>\n`));
3535

3636
switch (options.rule.type) {
3737
case "contentBased": {
3838
const flaggedMessage = options.messages[0];
39-
contentString += makeLogMessageTitleAndContent(
40-
"Flagged Message",
41-
`\`\n\n${flaggedMessage.content}\`\n`
39+
content.push(
40+
makeLogMessageTitleAndContent("Flagged Message", `\`\n\n${flaggedMessage.content}\`\n`)
4241
);
43-
contentString += SPACER;
44-
contentString += makeLogMessageTitleAndContent("Channel", `<#${flaggedMessage.channelId}>`);
42+
content.push(SPACER);
43+
content.push(makeLogMessageTitleAndContent("Channel", `<#${flaggedMessage.channelId}>`));
4544
break;
4645
}
4746
case "crossChannel": {
4847
if (options.rule.isBrokenBy.name === "isCrossPost") {
49-
contentString += `Posted in **${options.rule.channelCount}** channels within **${timeToString(options.rule.timeframe)} **\n`;
48+
content.push(
49+
`Posted in **${options.rule.channelCount}** channels within **${timeToString(options.rule.timeframe)} **\n`
50+
);
5051
const flaggedMessage = options.messages[0];
5152
const affectedChannels = new Set(options.messages.map((message) => message.channelId));
52-
contentString += makeLogMessageTitleAndContent(
53-
"Flagged Message",
54-
`\n\n${flaggedMessage.content}\n`
55-
);
56-
contentString += SPACER;
57-
contentString += makeLogMessageTitleAndContent(
58-
"Channels Involved",
59-
Array.from(affectedChannels)
60-
.map((id) => `<#${id}>`)
61-
.join(", ")
53+
const hasMessage = flaggedMessage.content && flaggedMessage.content.trim().length > 0;
54+
const hasAttachments = flaggedMessage.attachments.size > 0;
55+
if (hasMessage) {
56+
content.push(
57+
makeLogMessageTitleAndContent("Flagged Message", `\n\n${flaggedMessage.content}\n`)
58+
);
59+
}
60+
if (hasAttachments) {
61+
content.push(
62+
makeLogMessageTitleAndContent(
63+
"Flagged Message",
64+
`\n\n[Attachment: ${flaggedMessage.attachments.first()?.name}]\n`
65+
)
66+
);
67+
}
68+
if (!hasMessage && !hasAttachments) {
69+
content.push(makeLogMessageTitleAndContent("Flagged Message", `\n\n[No Text Content]\n`));
70+
}
71+
content.push(SPACER);
72+
content.push(
73+
makeLogMessageTitleAndContent(
74+
"Channels Involved",
75+
Array.from(affectedChannels)
76+
.map((id) => `<#${id}>`)
77+
.join(", ")
78+
)
6279
);
6380
}
6481
break;
6582
}
6683
case "frequencyBased": {
67-
contentString += `Sent **${options.rule.frequency}** messages within **${timeToString(options.rule.timeframe)}**\n`;
84+
content.push(
85+
`Sent **${options.rule.frequency}** messages within **${timeToString(options.rule.timeframe)}**\n`
86+
);
6887
const displayedMessages = options.messages.slice(0, 5);
6988
const displayedCount = displayedMessages.length;
7089
const remainingCount = options.messages.length - displayedCount;
7190

72-
contentString += `**Messages Involved:**\n`;
73-
contentString += displayedMessages
74-
.map((message) => {
75-
const contentPreview =
76-
message.content.length > 50 ? `${message.content.slice(0, 47)}...` : message.content;
77-
return `- ${contentPreview}`;
78-
})
79-
.join("\n");
91+
content.push(`**Messages Involved:**\n`);
92+
content.push(
93+
displayedMessages
94+
.map((message) => {
95+
const contentPreview =
96+
message.content.length > 50 ? `${message.content.slice(0, 47)}...` : message.content;
97+
return `- ${contentPreview}`;
98+
})
99+
.join("\n")
100+
);
80101
if (remainingCount > 0) {
81-
contentString += `\n ...and ${remainingCount} more\n`;
102+
content.push(`\n ...and ${remainingCount} more\n`);
82103
}
83104
break;
84105
}
85106
}
86107

87-
contentString += SPACER;
88-
contentString += "**Action(s) Taken:**\n";
108+
content.push(SPACER);
109+
content.push("**Action(s) Taken:**\n");
89110
if (options.deletedMessagesCount > 0) {
90-
contentString += `- Deleted ${options.deletedMessagesCount} message${options.deletedMessagesCount > 1 ? "s" : ""}\n`;
111+
content.push(
112+
`- Deleted ${options.deletedMessagesCount} message${options.deletedMessagesCount > 1 ? "s" : ""}\n`
113+
);
91114
}
92115
if (options.muteDuration) {
93-
contentString += `- Muted user for ${timeToString(options.muteDuration)}\n`;
116+
content.push(`- Muted user for ${timeToString(options.muteDuration)}\n`);
94117
}
95118

96-
return contentString;
119+
return content.join("");
97120
};
98121

99122
export const defaultLogFunction: LogFunction = async (options) => {

0 commit comments

Comments
 (0)