11from __future__ import annotations
22
3+ import codecs
34from dataclasses import dataclass
45import 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+
137172PROFANITY_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+
187233def _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