profanity

master
Denis Ranneft 1 month ago
parent de8e6bc892
commit 480dc591ef

@ -0,0 +1,35 @@
package handler
import (
"strings"
"testing"
"unicode/utf8"
"golang.org/x/text/unicode/norm"
)
func TestIsValidHeroName_NFCCombiningY(t *testing.T) {
// Й в NFD: и + U+0306; без NFC вторая руна не входит в блок кириллицы и имя отклонялось.
raw := "\u0438\u0306a"
nfc := norm.NFC.String(strings.TrimSpace(raw))
if nfc != "\u0439a" && utf8.RuneCountInString(nfc) != 2 {
t.Fatalf("unexpected NFC: %q (%d runes)", nfc, utf8.RuneCountInString(nfc))
}
if !isValidHeroName(nfc) {
t.Fatal("expected valid name with й (NFC)")
}
}
func TestIsValidHeroName_runeLengthNotBytes(t *testing.T) {
// 9 кириллических букв = 18 байт UTF-8, но 9 символов — должно быть допустимо.
name := "Абвгдейкz"
if utf8.RuneCountInString(name) != 9 {
t.Fatal("test setup")
}
if len(name) <= 16 {
t.Fatal("test needs >16 bytes with <=16 runes")
}
if !isValidHeroName(name) {
t.Fatal("expected valid: length limit is runes, not UTF-8 bytes")
}
}

@ -0,0 +1,72 @@
package profanity
import (
"strings"
goaway "github.com/TwiN/go-away"
)
var detector *goaway.ProfanityDetector
func init() {
combined := mergeUniqueWords(
goaway.DefaultProfanities,
russianProfanityFullWords,
latinRussianProfanityFull,
)
detector = goaway.NewProfanityDetector().WithCustomDictionary(
combined,
goaway.DefaultFalsePositives,
goaway.DefaultFalseNegatives,
)
}
func mergeUniqueWords(parts ...[]string) []string {
seen := make(map[string]struct{}, 768)
out := make([]string, 0, 768)
for _, p := range parts {
for _, w := range p {
if w == "" {
continue
}
if _, ok := seen[w]; ok {
continue
}
seen[w] = struct{}{}
out = append(out, w)
}
}
return out
}
// foldYoToE maps ё→е so dictionary entries using «е» still match nicknames typed with «ё».
func foldYoToE(s string) string {
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
switch r {
case 'ё', 'Ё':
b.WriteRune('е')
default:
b.WriteRune(r)
}
}
return b.String()
}
// HeroNameIsProfane: go-away (англ. + leet) + полные русские и латинские формы; две нормализации омоглифов.
func HeroNameIsProfane(name string) bool {
folded := foldYoToE(name)
if detector.IsProfane(folded) {
return true
}
if detector.IsProfane(cyrillicHomoglyphsToLatin(folded)) {
return true
}
if hasCyrillicLetter(folded) {
if detector.IsProfane(latinAndDigitHomoglyphsToCyrillic(folded)) {
return true
}
}
return false
}

@ -0,0 +1,54 @@
package profanity
import "testing"
func TestHeroNameIsProfane_english(t *testing.T) {
if HeroNameIsProfane("Alice") {
t.Fatal("expected clean name")
}
if !HeroNameIsProfane("fuckface") {
t.Fatal("expected English profanity")
}
}
func TestHeroNameIsProfane_russianPlain(t *testing.T) {
for _, n := range []string{"ХУЙ", "хуй", "ХуЙ", "хуйло", "пиздец", "сука"} {
if !HeroNameIsProfane(n) {
t.Fatalf("expected profane: %q", n)
}
}
}
func TestHeroNameIsProfane_khersonNotBlocked(t *testing.T) {
if HeroNameIsProfane("Херсон") {
t.Fatal("did not expect city name to be blocked")
}
}
func TestHeroNameIsProfane_mixedLatinCyrillic(t *testing.T) {
cases := []string{
"xуй", // лат. x + кирил. уй
"хyй", // кирил. х + лат. y + кирил. й
"hуй", // лат. h → х
"xyи", // лат. xy + кирил. и
"cука", // лат. c → с
}
for _, n := range cases {
if !HeroNameIsProfane(n) {
t.Fatalf("expected profane (mixed script): %q", n)
}
}
}
func TestHeroNameIsProfane_digitHomoglyphs(t *testing.T) {
// смешанный ник: кириллица + цифра вместо буквы (3 → з)
if !HeroNameIsProfane("пи3да") {
t.Fatal("expected 3→з to form пизда")
}
}
func TestHeroNameIsProfane_cyrillicHomoglyphEnglish(t *testing.T) {
if !HeroNameIsProfane("fu\u0441k") { // кириллическая «с»
t.Fatal("expected spoof latin profanity to be caught")
}
}

@ -0,0 +1,96 @@
package profanity
import (
"strings"
"unicode"
)
// cyrillicHomoglyphsToLatin: кириллица, похожая на латиницу → латиница (обход «fuсс кириллической «с»).
func cyrillicHomoglyphsToLatin(s string) string {
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
if lat, ok := cyrillicToLatinConfusable[r]; ok {
b.WriteRune(lat)
continue
}
b.WriteRune(r)
}
return b.String()
}
// latinAndDigitHomoglyphsToCyrillic: латиница и цифры, похожие на кириллицу → кириллица.
// Для ников со смешанным вводом (есть хотя бы одна кириллическая буква).
func latinAndDigitHomoglyphsToCyrillic(s string) string {
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
if cy, ok := latinToCyrillicConfusable[r]; ok {
b.WriteRune(cy)
continue
}
if cy, ok := digitToCyrillicConfusable[r]; ok {
b.WriteRune(cy)
continue
}
b.WriteRune(unicode.ToLower(r))
}
return b.String()
}
func hasCyrillicLetter(s string) bool {
for _, r := range s {
if unicode.Is(unicode.Cyrillic, r) {
return true
}
}
return false
}
var cyrillicToLatinConfusable = map[rune]rune{
'А': 'a', 'а': 'a',
'В': 'b', 'в': 'b',
'Е': 'e', 'е': 'e',
'З': '3', 'з': '3',
'И': 'u', 'и': 'u',
'К': 'k', 'к': 'k',
'М': 'm', 'м': 'm',
'Н': 'h', 'н': 'h',
'О': 'o', 'о': 'o',
'Р': 'p', 'р': 'p',
'С': 'c', 'с': 'c',
'Т': 't', 'т': 't',
'У': 'y', 'у': 'y',
'Х': 'x', 'х': 'x',
'Ч': '4', 'ч': '4',
}
var latinToCyrillicConfusable = map[rune]rune{
'a': 'а', 'A': 'а',
'b': 'в', 'B': 'в',
'c': 'с', 'C': 'с',
'd': 'д', 'D': 'д',
'e': 'е', 'E': 'е',
'f': 'ф', 'F': 'ф',
'h': 'х', 'H': 'х',
'i': 'и', 'I': 'и',
'k': 'к', 'K': 'к',
'm': 'м', 'M': 'м',
'o': 'о', 'O': 'о',
'p': 'р', 'P': 'р',
's': 'с', 'S': 'с',
't': 'т', 'T': 'т',
'u': 'и', 'U': 'и',
'v': 'в', 'V': 'в',
'w': 'ш', 'W': 'ш',
'x': 'х', 'X': 'х',
'y': 'у', 'Y': 'у',
}
var digitToCyrillicConfusable = map[rune]rune{
'0': 'о',
'3': 'з',
'4': 'ч',
'6': 'б',
'8': 'в',
}

@ -0,0 +1,22 @@
package profanity
// latinRussianProfanityFull — популярные латинские написания и «транслит» русского мата (цельные слова).
var latinRussianProfanityFull = []string{
"ahuel", "ahuet", "ahui", "ahuj", "ahuy",
"blya", "blyad", "blyat", "blyadina",
"dermo", "dolboeb", "dolboyob",
"ebal", "ebalo", "eban", "ebany", "ebat", "eblan", "eblo", "ebnut",
"gandon", "govno", "govnyuk",
"hren", "hrenov", "hui", "huj", "huy", "hue", "huilo", "huinya", "huesos",
"jebat", "jopa", "mudak", "mudilo",
"nahuja", "nahui", "nahuj", "nahuy", "naher", "nafig", "nafiga",
"ohuel", "ohuet", "ohueno", "oxuenno", "oxuel",
"pedik", "pedril", "pidar", "pidor", "pidoras", "pidrila",
"pizda", "pizdec", "pizdetc", "pizduk", "pizdobol", "pohui", "pohuj", "pohuy", "poher",
"sran", "srat", "suka", "suchka", "svoloch",
"trahat", "trahnut", "traxat", "traxnut",
"ueban", "uebok", "uebishche", "vyeban", "vyebon",
"xren", "xrenya", "xyi", "xyu", "xyulo", "xyesos",
"zaeb", "zaebal", "zaebis", "zalupa",
"yebat", "yeban", "yeblo",
}

@ -0,0 +1,29 @@
package profanity
// russianProfanityFullWords — цельные слова и устойчивые формы (нижний регистр, «е» вместо «ё»).
// Без коротких корней вроде «пизд», «хер» — чтобы не резать нормальные имена («Херсон» и т.д.).
var russianProfanityFullWords = []string{
"ахуеть", "ахуенно", "ахуительно",
"блядина", "блядский", "блядство", "блядь", "блять",
"въебать", "выебать", "выебон", "выебывается",
"гандон", "говно", "говнюк", "говнюшка", "говенный",
"дерьмо", "дерьмоед", "долбоеб", "долбоящер", "доебаться",
"ебало", "ебальник", "ебанутый", "ебаный", "ебарь", "ебать", "ебаться", "еблан", "ебло", "ебнутый", "ебучий",
"жопа", "жопник", "жополиз",
"залупа", "залупень", "залупонос", "заебал", "заебись", "заебать", "засранец",
"изъебнуться",
"мразь", "мразота", "мразишка", "мудак", "мудацкий", "мудила", "мудозвон", "мудень",
"нахуй", "нафиг",
"охуеть", "охуенно", "охуительно", "охуевший",
"педик", "педрила",
"пидор", "пидарас", "пидорас", "пидрила",
"пизда", "пиздец", "пиздюк", "пиздобол", "пиздюля", "пиздун", "пиздострадалец", "пиздануть", "пиздатый",
"похуй", "подъебать", "подъебнуть",
"сволочь", "сосунок", "сраный", "срать", "ссанина", "ссыкун", "сука", "сучара", "сученыш", "сучий",
"тварь", "трахать", "трахнуть",
"уебан", "уебище", "уебок",
"херня", "хренов", "хреново",
"хуй", "хуя", "хую", "хуе", "хуи", "хуйня", "хуйло", "хуесос", "хуеглот", "хуеплет", "хуиндец",
"шлюха", "шлюшка", "шлюхин",
"скотина", "ублюдок",
}
Loading…
Cancel
Save