From 5ae9307636ed763f836bdbc6702012f5d9d781cb Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Thu, 9 Nov 2006 00:24:22 +0000 Subject: [PATCH] Factored the curse filter out into a reusable class. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@4446 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- .../crowd/chat/client/CurseFilter.java | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 src/java/com/threerings/crowd/chat/client/CurseFilter.java diff --git a/src/java/com/threerings/crowd/chat/client/CurseFilter.java b/src/java/com/threerings/crowd/chat/client/CurseFilter.java new file mode 100644 index 000000000..5255d0f57 --- /dev/null +++ b/src/java/com/threerings/crowd/chat/client/CurseFilter.java @@ -0,0 +1,223 @@ +// +// $Id$ + +package com.threerings.crowd.chat.client; + +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.samskivert.util.RandomUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.util.Name; + +import com.threerings.crowd.Log; + +/** + * A chat filter that can filter out curse words from user chat. + */ +public abstract class CurseFilter implements ChatFilter +{ + /** Indicates how messages should be handled. */ + public enum Mode { DROP, COMIC, VERNACULAR, UNFILTERED; } + + /** + * Creates a curse filter. The curse words should be a string in the + * following format: + * + *
+     * *penis*=John_Thomas shit*=barnacle muff=britches
+     * 
+ * + * The key/value pairs are separated by spaces, * matches word characters + * and the value after the = is the string into which to convert the text + * when converting to the vernacular. Underscores in the target string will + * be turned into spaces. + * + *

And stopWords should be in the following format: + * + *

+     * *faggot* rape rapes raped raping
+     * 
+ * + * Words are separated by spaces and * matches any other word characters. + */ + public CurseFilter (String curseWords, String stopWords) + { + configureCurseWords(curseWords); + configureStopWords(stopWords); + } + + /** + * The client will need to provide a way to look up our current chat filter + * mode. + */ + public abstract Mode getFilterMode (); + + // from interface ChatFilter + public String filter (String msg, Name otherUser, boolean outgoing) + { + // first, check against the drop-always list + _stopMatcher.reset(msg); + if (_stopMatcher.matches()) { + return null; + } + + // then see what kind of curse filtering the user has configured + Mode level = getFilterMode(); + if (level == Mode.UNFILTERED) { + return msg; + } + + int numStops = _matchers.length; + StringBuffer inbuf = new StringBuffer(msg); + StringBuffer outbuf = new StringBuffer(msg.length()); + for (int ii=0, nn=_matchers.length; ii < nn; ii++) { + Matcher m = _matchers[ii]; + m.reset(inbuf); + while (m.find()) { + switch (level) { + case DROP: + return null; + + case COMIC: default: + m.appendReplacement(outbuf, + StringUtil.replace(_replacements[ii], " ", + comicChars(_comicLength[ii]))); + break; + + case VERNACULAR: + String vernacular = _vernacular[ii]; + if (Character.isUpperCase(m.group(2).charAt(0))) { + vernacular = new String( + Character.toUpperCase(vernacular.charAt(0)) + + vernacular.substring(1)); + } + m.appendReplacement(outbuf, + StringUtil.replace(_replacements[ii], " ", vernacular)); + break; + } + } + if (outbuf.length() == 0) { + // optimization: if we didn't find a match, jump to the next + // pattern without doing any StringBuilder jimmying + continue; + } + m.appendTail(outbuf); + + // swap the buffers around and clear the output + StringBuffer temp = inbuf; + inbuf = outbuf; + outbuf = temp; + outbuf.setLength(0); + } + + return inbuf.toString(); + } + + /** + * Configure the curse word portion of our filtering. + */ + protected void configureCurseWords (String curseWords) + { + StringTokenizer st = new StringTokenizer(curseWords); + int numWords = st.countTokens(); + + _matchers = new Matcher[numWords]; + _replacements = new String[numWords]; + _vernacular = new String[numWords]; + _comicLength = new int[numWords]; + + for (int ii=0; ii < numWords; ii++) { + String mapping = st.nextToken(); + StringTokenizer st2 = new StringTokenizer(mapping, "="); + if (st2.countTokens() != 2) { + Log.warning("Something looks wrong in the x.cursewords " + + "properties (" + mapping + "), skipping."); + continue; + } + String curse = st2.nextToken(); + + String s = ""; + String p = ""; + if (curse.startsWith("*")) { + curse = curse.substring(1); + p += "([a-zA-Z]*)"; + s += "$1"; + } else { + p += "()"; + } + s += " "; + p += " "; + if (curse.endsWith("*")) { + curse = curse.substring(0, curse.length() - 1); + p += "([a-zA-Z]*)"; + s += "$3"; + } + + String pattern = "\\b" + + StringUtil.replace(p, " ", "(" + curse + ")") + "\\b"; + Pattern pat = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + _matchers[ii] = pat.matcher(""); + _replacements[ii] = s; + _vernacular[ii] = st2.nextToken().replace('_', ' '); + _comicLength[ii] = curse.length(); + } + } + + /** + * Configure the words that will stop. + */ + protected void configureStopWords (String stopWords) + { + StringTokenizer st = new StringTokenizer(stopWords); + + String pattern = ""; + while (st.hasMoreTokens()) { + if ("".equals(pattern)) { + pattern += ".*("; + } else { + pattern += "|"; + } + pattern += "\\b" + + StringUtil.replace(st.nextToken(), "*", "[A-Za-z]*") + "\\b"; + } + pattern += ").*"; + + Pattern pat = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + _stopMatcher = pat.matcher(""); + } + + /** + * Return a comicy replacement of the specified length; + */ + protected String comicChars (int length) + { + StringBuilder buf = new StringBuilder(); + for (int ii=0; ii < length; ii++) { + buf.append(RandomUtil.pickRandom(COMIC_CHARS)); + } + + return buf.toString(); + } + + /** A matcher that will always cause a message to be dropped if it + * matches. */ + protected Matcher _stopMatcher; + + /** Matchers for each curseword. */ + protected Matcher[] _matchers; + + /** Length of comic-y replacements for each curseword. */ + protected int[] _comicLength; + + /** Replacements. */ + protected String[] _replacements; + + /** Replacements for each curseword "in the vernacular". */ + protected String[] _vernacular; + + /** Comic replacement characters. */ + protected String[] COMIC_CHARS = { "!", "@", "#", "%", "&", "*" }; +}