Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Add displayname mention spam protection (#537)" #571

Merged
merged 2 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
"humanize-duration-ts": "^2.1.1",
"js-yaml": "^4.1.0",
"jsdom": "^16.6.0",
"lru-cache": "^11.0.1",
"matrix-appservice-bridge": "10.3.1",
"nsfwjs": "^4.1.0",
"parse-duration": "^1.0.2",
Expand All @@ -70,6 +69,5 @@
},
"engines": {
"node": ">=20.0.0"
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
}
61 changes: 2 additions & 59 deletions src/protections/MentionSpam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,10 @@ import { Protection } from "./IProtection";
import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService, Permalinks, UserID } from "@vector-im/matrix-bot-sdk";
import { NumberProtectionSetting } from "./ProtectionSettings";
import { LRUCache } from "lru-cache";

export const DEFAULT_MAX_MENTIONS = 10;

export class MentionSpam extends Protection {
private roomDisplaynameCache = new LRUCache<string, string[]>({
ttl: 1000 * 60 * 24, // 24 minutes
ttlAutopurge: true,
});

settings = {
maxMentions: new NumberProtectionSetting(DEFAULT_MAX_MENTIONS, 1),
Expand All @@ -43,29 +38,7 @@ export class MentionSpam extends Protection {
return "If a user posts many mentions, that message is redacted. No bans are issued.";
}

private async getRoomDisplaynames(mjolnir: Mjolnir, roomId: string): Promise<string[]> {
const existing = this.roomDisplaynameCache.get(roomId);
if (existing) {
return existing;
}
const profiles = await mjolnir.client.getJoinedRoomMembersWithProfiles(roomId);
const displaynames = (
Object.values(profiles)
.map((v) => v.display_name?.toLowerCase())
.filter((v) => typeof v === "string") as string[]
)
// Limit to displaynames with more than a few characters.
.filter((displayname) => displayname.length > 2);

this.roomDisplaynameCache.set(roomId, displaynames);
return displaynames;
}

public checkMentions(
body: unknown | undefined,
htmlBody: unknown | undefined,
mentionArray: unknown | undefined,
): boolean {
public checkMentions(body: unknown|undefined, htmlBody: unknown|undefined, mentionArray: unknown|undefined): boolean {
const max = this.settings.maxMentions.value;
if (Array.isArray(mentionArray) && mentionArray.length > max) {
return true;
Expand All @@ -79,41 +52,11 @@ export class MentionSpam extends Protection {
return false;
}

public checkDisplaynameMentions(
body: unknown | undefined,
htmlBody: unknown | undefined,
displaynames: string[],
): boolean {
const max = this.settings.maxMentions.value;
const bodyWords = ((typeof body === "string" && body) || "").toLowerCase();
if (displaynames.filter((s) => bodyWords.includes(s.toLowerCase())).length > max) {
return true;
}
const htmlBodyWords = decodeURIComponent((typeof htmlBody === "string" && htmlBody) || "").toLowerCase();
if (displaynames.filter((s) => htmlBodyWords.includes(s)).length > max) {
return true;
}
return false;
}

public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
if (event["type"] === "m.room.message") {
let content = event["content"] || {};
const explicitMentions = content["m.mentions"]?.user_ids;
let hitLimit = this.checkMentions(content.body, content.formatted_body, explicitMentions);

// Slightly more costly to hit displaynames, so only do it if we don't hit on mxid matches.
if (!hitLimit) {
const displaynames = await this.getRoomDisplaynames(mjolnir, roomId);
hitLimit = this.checkDisplaynameMentions(content.body, content.formatted_body, displaynames);
if (hitLimit) {
LogService.info(
"MentionSpam",
`Hitlimit reached via display name mention check for event content ${JSON.stringify(content)}`,
);
}
}

const hitLimit = this.checkMentions(content.body, content.formatted_body, explicitMentions);
if (hitLimit) {
await mjolnir.managementRoomOutput.logMessage(
LogLevel.WARN,
Expand Down
36 changes: 4 additions & 32 deletions test/integration/mentionSpamProtectionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,26 +94,17 @@ describe("Test: Mention spam protection", function () {
});
});
// Also covers HTML mentions
const mentionUsers = Array.from({ length: DEFAULT_MAX_MENTIONS + 1 }, (_, i) => `@user${i}:example.org`);
const mentionDisplaynames = Array.from({ length: DEFAULT_MAX_MENTIONS + 1 }, (_, i) => `Test User ${i}`);

// Pre-set the displayname cache.
let protection = this.mjolnir.protectionManager.protections.get("MentionSpam");
protection.roomDisplaynameCache.set(room, mentionDisplaynames);

const messageWithTextMentions = await client.sendText(room, mentionUsers.join(" "));
const messageWithHTMLMentions = await client.sendHtmlText(
room,
mentionUsers.map((u) => `<a href=\"https://matrix.to/#/${encodeURIComponent(u)}\">${u}</a>`).join(" "),
);
const mentionUsers = Array.from({length: DEFAULT_MAX_MENTIONS+1}, (_, i) => `@user${i}:example.org`);
const messageWithTextMentions = await client.sendText(room, mentionUsers.join(' '));
const messageWithHTMLMentions = await client.sendHtmlText(room,
mentionUsers.map(u => `<a href=\"https://matrix.to/#/${encodeURIComponent(u)}\">${u}</a>`).join(' '));
const messageWithMMentions = await client.sendMessage(room, {
msgtype: "m.text",
body: "Hello world",
["m.mentions"]: {
user_ids: mentionUsers,
},
});
const messageWithDisplaynameMentions = await client.sendText(room, mentionDisplaynames.join(" "));

await delay(500);

Expand All @@ -125,24 +116,5 @@ describe("Test: Mention spam protection", function () {

const fetchedMentionsEvent = await client.getEvent(room, messageWithMMentions);
assert.equal(Object.keys(fetchedMentionsEvent.content).length, 0, "This event should have been redacted");

const fetchedDisplaynameEvent = await client.getEvent(room, messageWithDisplaynameMentions);
assert.equal(Object.keys(fetchedDisplaynameEvent.content).length, 0, "This event should have been redacted");

// send messages after activating protection, they should be auto-redacted
const messages = [];
for (let i = 0; i < 10; i++) {
let nextMessage = await client.sendText(room, `hello${i}`);
messages.push(nextMessage);
}

messages.forEach(async (eventID) => {
await client.getEvent(room, eventID);
assert.equal(
Object.keys(fetchedDisplaynameEvent.content).length,
0,
"This event should have been redacted",
);
});
});
});
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2578,11 +2578,6 @@ lru-cache@^10.0.1:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==

lru-cache@^11.0.1:
version "11.0.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.1.tgz#3a732fbfedb82c5ba7bca6564ad3f42afcb6e147"
integrity sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==

lru-cache@^4.1.5:
version "4.1.5"
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz"
Expand Down
Loading