Skip to content

Commit 32124ed

Browse files
authored
Improve and stabalise the mention limit protection. (#844)
- Send a warning message when the event gets removed. - Ban on the second infraction. - Make it an option as to whether the message gets split. - The config file won't work anymore can't fix that because wuh we can't have both as the source of truth........ unless we differentiate based on the timestamp but that requires infrastructure changes.
1 parent 62163a4 commit 32124ed

File tree

4 files changed

+236
-150
lines changed

4 files changed

+236
-150
lines changed

src/config.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,6 @@ export interface IConfig {
121121
serverNames: string[];
122122
banMessage: string;
123123
};
124-
mentionLimitProtection: {
125-
maxMentions: number;
126-
redactReason: string;
127-
};
128124
};
129125
safeMode?: {
130126
bootOption: SafeModeBootOption;
@@ -230,11 +226,6 @@ const defaultConfig: IConfig = {
230226
banMessage:
231227
"Unfortunately we cannot accept new users from your homeserver at this time.",
232228
},
233-
mentionLimitProtection: {
234-
maxMentions: 3,
235-
redactReason:
236-
"You have mentioned too many users in this message, so we have had to redact it.",
237-
},
238229
},
239230
safeMode: {
240231
bootOption: SafeModeBootOption.RecoveryOnly,

src/protections/MentionLimitProtection.ts

Lines changed: 0 additions & 137 deletions
This file was deleted.
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// Copyright 2024 - 2025 Gnuxie <[email protected]>
2+
// Copyright 2024 The Matrix.org Foundation C.I.C.
3+
//
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
import {
7+
AbstractProtection,
8+
ActionResult,
9+
EDStatic,
10+
EventConsequences,
11+
Logger,
12+
Ok,
13+
ProtectedRoomsSet,
14+
Protection,
15+
ProtectionDescription,
16+
RoomEvent,
17+
RoomMessageSender,
18+
Task,
19+
UserConsequences,
20+
Value,
21+
describeProtection,
22+
isError,
23+
} from "matrix-protection-suite";
24+
import { Draupnir } from "../Draupnir";
25+
import {
26+
MatrixRoomID,
27+
StringUserID,
28+
} from "@the-draupnir-project/matrix-basic-types";
29+
import { Type } from "@sinclair/typebox";
30+
import { LazyLeakyBucket } from "../queues/LeakyBucket";
31+
import { sendMatrixEventsFromDeadDocument } from "../commands/interface-manager/MPSMatrixInterfaceAdaptor";
32+
import { DeadDocumentJSX } from "@the-draupnir-project/interface-manager";
33+
import { renderMentionPill } from "../commands/interface-manager/MatrixHelpRenderer";
34+
35+
const log = new Logger("MentionLimitProtection");
36+
37+
const MentionsContentSchema = Type.Object({
38+
"m.mentions": Type.Object({
39+
user_ids: Type.Array(Type.String()),
40+
}),
41+
});
42+
43+
const NewContentMentionsSchema = Type.Object({
44+
"m.new_content": MentionsContentSchema,
45+
});
46+
47+
const WeakTextContentSchema = Type.Object({
48+
body: Type.Optional(Type.String()),
49+
formatted_body: Type.Optional(Type.String()),
50+
});
51+
52+
export function isContainingMentionsOverLimit(
53+
event: RoomEvent,
54+
maxMentions: number,
55+
checkBody: boolean
56+
): boolean {
57+
const isOverLimit = (user_ids: string[]): boolean =>
58+
user_ids.length > maxMentions;
59+
if (
60+
Value.Check(NewContentMentionsSchema, event.content) &&
61+
isOverLimit(event.content["m.new_content"]["m.mentions"].user_ids)
62+
) {
63+
return true;
64+
}
65+
if (
66+
Value.Check(MentionsContentSchema, event.content) &&
67+
isOverLimit(event.content["m.mentions"].user_ids)
68+
) {
69+
return true;
70+
}
71+
if (checkBody && Value.Check(WeakTextContentSchema, event.content)) {
72+
if (
73+
event.content.body !== undefined &&
74+
event.content.body.split("@").length - 1 > maxMentions
75+
) {
76+
return true;
77+
}
78+
}
79+
return false;
80+
}
81+
82+
const MentionLimitProtectionSettings = Type.Object(
83+
{
84+
maxMentions: Type.Integer({
85+
description: "The maximum number of mentions permitted.",
86+
default: 3,
87+
}),
88+
warningText: Type.String({
89+
description:
90+
"The reason to use to notify the user after redacting their infringing message.",
91+
default:
92+
"You have mentioned too many users in this message, so we have had to redact it.",
93+
}),
94+
includeLegacyMentions: Type.Boolean({
95+
description:
96+
"Whether to scrape the body for legacy mentions, can lead to more false positives.",
97+
default: false,
98+
}),
99+
},
100+
{
101+
title: "MentionLimitProtectionSettings",
102+
}
103+
);
104+
105+
type MentionLimitProtectionSettings = EDStatic<
106+
typeof MentionLimitProtectionSettings
107+
>;
108+
109+
export type MentionLimitProtectionDescription = ProtectionDescription<
110+
unknown,
111+
typeof MentionLimitProtectionSettings,
112+
MentionLimitProtectionCapabilities
113+
>;
114+
115+
export class MentionLimitProtection
116+
extends AbstractProtection<MentionLimitProtectionDescription>
117+
implements Protection<MentionLimitProtectionDescription>
118+
{
119+
private readonly eventConsequences: EventConsequences;
120+
private readonly userConsequences: UserConsequences;
121+
private readonly warningText: string;
122+
private readonly maxMentions: number;
123+
private readonly includeLegacymentions: boolean;
124+
private readonly consequenceBucket = new LazyLeakyBucket<StringUserID>(
125+
1,
126+
30 * 60_000 // half an hour will do
127+
);
128+
constructor(
129+
description: MentionLimitProtectionDescription,
130+
capabilities: MentionLimitProtectionCapabilities,
131+
private readonly roomMessageSender: RoomMessageSender,
132+
protectedRoomsSet: ProtectedRoomsSet,
133+
settings: MentionLimitProtectionSettings
134+
) {
135+
super(description, capabilities, protectedRoomsSet, {});
136+
this.eventConsequences = capabilities.eventConsequences;
137+
this.userConsequences = capabilities.userConsequences;
138+
this.maxMentions = settings.maxMentions;
139+
this.warningText = settings.warningText;
140+
this.includeLegacymentions = settings.includeLegacyMentions;
141+
}
142+
public async handleTimelineEvent(
143+
_room: MatrixRoomID,
144+
event: RoomEvent
145+
): Promise<ActionResult<void>> {
146+
if (event.sender === this.protectedRoomsSet.userID) {
147+
return Ok(undefined);
148+
}
149+
if (
150+
isContainingMentionsOverLimit(
151+
event,
152+
this.maxMentions,
153+
this.includeLegacymentions
154+
)
155+
) {
156+
const infractions = this.consequenceBucket.getTokenCount(event.sender);
157+
if (infractions > 0) {
158+
const userResult = await this.userConsequences.consequenceForUserInRoom(
159+
event.room_id,
160+
event.sender,
161+
this.warningText
162+
);
163+
if (isError(userResult)) {
164+
log.error("Failed to ban the user", event.sender, userResult.error);
165+
}
166+
// fall through to the event consequence on purpose so we redact the event too.
167+
} else {
168+
// if they're not being banned we need to tell them why their message got redacted.
169+
void Task(
170+
sendMatrixEventsFromDeadDocument(
171+
this.roomMessageSender,
172+
event.room_id,
173+
<root>
174+
{renderMentionPill(event.sender, event.sender)} {this.warningText}
175+
</root>,
176+
{ replyToEvent: event }
177+
),
178+
{
179+
log,
180+
}
181+
);
182+
}
183+
this.consequenceBucket.addToken(event.sender);
184+
return await this.eventConsequences.consequenceForEvent(
185+
event.room_id,
186+
event.event_id,
187+
this.warningText
188+
);
189+
} else {
190+
return Ok(undefined);
191+
}
192+
}
193+
}
194+
195+
export type MentionLimitProtectionCapabilities = {
196+
eventConsequences: EventConsequences;
197+
userConsequences: UserConsequences;
198+
};
199+
200+
describeProtection<
201+
MentionLimitProtectionCapabilities,
202+
Draupnir,
203+
typeof MentionLimitProtectionSettings
204+
>({
205+
name: "MentionLimitProtection",
206+
description: `A potection that will remove any messages with
207+
a number of mentions over a preconfigured limit.
208+
Please read the documentation https://the-draupnir-project.github.io/draupnir-documentation/protections/mention-limit-protection.`,
209+
capabilityInterfaces: {
210+
eventConsequences: "EventConsequences",
211+
userConsequences: "UserConsequences",
212+
},
213+
defaultCapabilities: {
214+
eventConsequences: "StandardEventConsequences",
215+
userConsequences: "StandardUserConsequences",
216+
},
217+
configSchema: MentionLimitProtectionSettings,
218+
factory: (decription, protectedRoomsSet, draupnir, capabilitySet, settings) =>
219+
Ok(
220+
new MentionLimitProtection(
221+
decription,
222+
capabilitySet,
223+
draupnir.clientPlatform.toRoomMessageSender(),
224+
protectedRoomsSet,
225+
settings
226+
)
227+
),
228+
});

0 commit comments

Comments
 (0)