Skip to content

Commit 7504bbc

Browse files
committed
Hard block protected-class slurs locally
1 parent e556f6a commit 7504bbc

2 files changed

Lines changed: 54 additions & 3 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ This repository's source code is open source under the MIT license. See
4444
- native Discord AutoMod rules for spam, mention raids, hate-speech presets,
4545
and known scam-link patterns
4646
- local Memact Guard deletion for strong profanity, offensive extremist or
47-
dictator references, untrusted external links, suspicious promos,
48-
unsolicited invites, URL shorteners, and repeated spam, including messages
49-
from installed bots/apps and webhooks
47+
dictator references, protected-class slurs, untrusted external links,
48+
suspicious promos, unsolicited invites, URL shorteners, and repeated spam,
49+
including messages from installed bots/apps and webhooks
5050
- silent Sentinel detection for protected-class hate patterns, self-harm
5151
harassment, scam links, homoglyph domains, misleading markdown links,
5252
new-account bursts, and mention raids
@@ -123,6 +123,8 @@ In practice:
123123
- Memact's local guard deletes public strong-profanity and offensive-reference
124124
messages while intentionally allowing short casual terms such as `wth` and
125125
`wtf`
126+
- protected-class slurs are hard-blocked locally with punctuation and leetspeak
127+
variants, so targeted racism does not depend only on Discord's native preset
126128
- public links use an allowlist-first posture: known safe hosts stay normal,
127129
while unknown external links, shorteners, suspicious TLDs, invite spam, and
128130
random-looking hosts are treated as spam-risk and logged

utils/content_guard.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import codecs
34
from dataclasses import dataclass
45
import re
56

@@ -88,14 +89,22 @@ def _letter_pattern(word: str) -> str:
8889
piece = r"[a@4]+"
8990
elif char == "e":
9091
piece = r"[e3]+"
92+
elif char == "g":
93+
piece = r"[g9q]+"
9194
elif char == "i":
9295
piece = r"[i1!|]+"
96+
elif char == "l":
97+
piece = r"[l1!|]+"
9398
elif char == "o":
9499
piece = r"[o0]+"
100+
elif char == "b":
101+
piece = r"[b8]+"
95102
elif char == "s":
96103
piece = r"[s$5]+"
97104
elif char == "t":
98105
piece = r"[t7]+"
106+
elif char == "z":
107+
piece = r"[z2]+"
99108
else:
100109
piece = re.escape(char) + "+"
101110
pieces.append(piece)
@@ -134,6 +143,32 @@ def _letter_pattern(word: str) -> str:
134143
"osama bin laden",
135144
)
136145

146+
# Encoded so source/docs do not render slurs while still keeping the detector local and deterministic.
147+
PROTECTED_CLASS_SLURS_ROT13 = (
148+
"avttre",
149+
"avttn",
150+
"snttbg",
151+
"snt",
152+
"xvxr",
153+
"puvax",
154+
"tbbx",
155+
"fcvp",
156+
"jrgonpx",
157+
"ornare",
158+
"erqfxva",
159+
"ergneq",
160+
"ergneqrq",
161+
"genaal",
162+
"qlxr",
163+
"furznyr",
164+
"pbba",
165+
"gbjryurnq",
166+
"enturnq",
167+
"mvccreurnq",
168+
"cbepu zbaxrl",
169+
"zbatbybvq",
170+
)
171+
137172
PROFANITY_PATTERNS = tuple(
138173
re.compile(rf"(?<![a-z0-9]){_letter_pattern(term)}(?:s|ed|ing|er|ers)?(?![a-z0-9])", re.IGNORECASE)
139174
for term in BLOCKED_PROFANITY
@@ -142,6 +177,13 @@ def _letter_pattern(word: str) -> str:
142177
re.compile(rf"(?<![a-z0-9]){_letter_pattern(term)}(?![a-z0-9])", re.IGNORECASE)
143178
for term in OFFENSIVE_REFERENCES
144179
)
180+
PROTECTED_CLASS_SLUR_PATTERNS = tuple(
181+
re.compile(
182+
rf"(?<![a-z0-9]){_letter_pattern(codecs.decode(term, 'rot_13'))}(?:s|ed|ing|er|ers)?(?![a-z0-9])",
183+
re.IGNORECASE,
184+
)
185+
for term in PROTECTED_CLASS_SLURS_ROT13
186+
)
145187

146188

147189
@dataclass(frozen=True)
@@ -184,6 +226,10 @@ def _contains_offensive_reference(normalized: str) -> bool:
184226
return any(pattern.search(normalized) for pattern in OFFENSIVE_REFERENCE_PATTERNS)
185227

186228

229+
def _contains_protected_class_slur(normalized: str) -> bool:
230+
return any(pattern.search(normalized) for pattern in PROTECTED_CLASS_SLUR_PATTERNS)
231+
232+
187233
def _host_tld(host: str) -> str:
188234
parts = host.rsplit(".", 1)
189235
return parts[1] if len(parts) == 2 else ""
@@ -291,6 +337,9 @@ def evaluate_guard_message(
291337
for signal in silent_decision.signals:
292338
_append_unique(signals, signal)
293339

340+
if _contains_protected_class_slur(normalized):
341+
signals.append(SentinelSignal("hate_speech", "protected-class slur", 5, 0.97))
342+
294343
if _contains_blocked_profanity(normalized):
295344
signals.append(SentinelSignal("profanity", "blocked profanity", 2, 0.92))
296345

0 commit comments

Comments
 (0)