Moderacja wyrazow: slownik i wstrzymywanie wiadomosci do akceptacji
Deploy PROD / deploy (push) Successful in 33s

Wiadomosc czatu i pytanie uczestnika przechodza przez slownik przed
publikacja. Trafienie wstrzymuje je (status NEW, moderation_hit), widzi
je tylko panel prowadzenia z przyciskami Opublikuj / Odrzuc, a autor
dostaje informacje, ze wiadomosc czeka. Wiadomosci prowadzacych nie sa
filtrowane.

Normalizacja rozpoznaje ogonki, wielkie litery, cyfry zamiast liter,
powtorzenia i rozstrzelenia. Slownik startowy w V31: wulgaryzmy, obelgi,
tresci zwiazane z pedofilia. WordFilterTest czyta slownik z migracji
i sprawdza obie strony, takze slownictwo wodociagowe.

Ekran "Moderacja wyrazow" w menu: lista z wyszukiwaniem i kategoriami,
dodawanie, edycja, wylaczanie, usuwanie z potwierdzeniem, sprawdzanie
tekstu. Dostep ADMIN i MODERATOR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 09:48:39 +02:00
parent c53ad95104
commit dda54f00a1
22 changed files with 1457 additions and 16 deletions
@@ -0,0 +1,124 @@
package com.softspm.webinar.moderation;
import com.softspm.webinar.domain.ModerationCategory;
import com.softspm.webinar.domain.ModerationMatchMode;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Filtr slownika moderacji, sprawdzany na slowniku startowym z migracji.
*
* Slownik czytany jest wprost z pliku V31, a nie przepisany do testu: test
* sprawdza dokladnie to, co trafi na kazda nowa instancje.
*
* Obie strony sa rownie wazne. Filtr, ktory przepuszcza "k.u.r.w.a", nie
* chroni sali; filtr, ktory wstrzymuje "duplikat faktury" albo "ciotka",
* zasypuje prowadzacego falszywymi alarmami, az przestanie je czytac.
*/
class WordFilterTest {
private static final List<WordFilter.Entry> SEED = loadSeed();
private static List<WordFilter.Entry> loadSeed() {
try {
String sql = Files.readString(
Path.of("src/main/resources/db/migration/V31__moderation_words.sql"),
StandardCharsets.UTF_8);
// tylko czesc INSERT: wyzej sa listy wartosci ograniczen CHECK
String inserts = sql.substring(sql.indexOf("INSERT INTO moderation_words"));
Matcher m = Pattern.compile("\\('([^']+)', '(\\w+)', '(\\w+)'\\)").matcher(inserts);
List<WordFilter.Entry> entries = new ArrayList<>();
while (m.find()) {
entries.add(WordFilter.Entry.of(m.group(1),
ModerationCategory.valueOf(m.group(2)),
ModerationMatchMode.valueOf(m.group(3))));
}
return entries;
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
@Test
void seedIsLoaded() {
assertThat(SEED).hasSizeGreaterThan(50);
}
@ParameterizedTest
@ValueSource(strings = {
"ty kurwo",
"KURWA",
"Kurwą jesteś",
"kuuuurwa",
"k.u.r.w.a",
"k u r w a",
"kurw4",
"to jest zajebiste",
"spierdalaj stąd",
"wypierdalaj",
"ty debilu",
"chuj z tym",
"ch_uj",
"sp13rd0l",
"pizda",
})
void holdsVulgarAndInsults(String text) {
assertThat(WordFilter.find(text, SEED)).as(text).isNotNull();
}
@ParameterizedTest
@ValueSource(strings = {
"pedofil",
"Pedofilia to przestępstwo",
"pornografia dziecięca",
"szukam pornografii dziecięcej",
"seks z dziećmi",
})
void holdsChildAbuse(String text) {
WordFilter.Hit hit = WordFilter.find(text, SEED);
assertThat(hit).as(text).isNotNull();
assertThat(hit.category()).isEqualTo(ModerationCategory.CHILD_ABUSE);
}
@ParameterizedTest
@ValueSource(strings = {
"Dzień dobry, mam pytanie o przyłącze wodociągowe",
"Proszę o duplikat faktury",
"Moja ciotka też pracuje w wodociągach",
"Chodzi głównie o przepompownię ścieków",
"Gnojowica i osady ściekowe",
"Jaki jest dopuszczalny poziom chloru?",
"Hydrant przy ulicy Kurkowej",
"principal i sprawozdanie finansowe",
"Szkolenie z kanalizacji deszczowej",
"Czy będzie nagranie?",
"Dziękuję, świetny webinar!",
})
void letsOrdinaryMessagesThrough(String text) {
assertThat(WordFilter.find(text, SEED)).as(text).isNull();
}
@Test
void speakerWordsAreNotSpecialHere() {
// filtr nie wie, kto pisze: pomijanie prowadzacych robi InteractionService
assertThat(WordFilter.find("kurwa", SEED)).isNotNull();
}
@Test
void emptyTextPasses() {
assertThat(WordFilter.find("", SEED)).isNull();
assertThat(WordFilter.find(" ", SEED)).isNull();
assertThat(WordFilter.find(null, SEED)).isNull();
}
}