From b7944bcee4f5ff70dc80d0042623b66ca1fd942e Mon Sep 17 00:00:00 2001 From: Magic fTail Date: Mon, 18 Mar 2019 18:00:35 +0100 Subject: [PATCH] Add chat filter plugin Add a plugin that censors user defined words/sentences, changes the message, or removes the entire message containing them. Co-authored-by: Adam --- .../plugins/chatfilter/ChatFilterConfig.java | 66 ++++++ .../plugins/chatfilter/ChatFilterPlugin.java | 214 ++++++++++++++++++ .../plugins/chatfilter/ChatFilterType.java | 43 ++++ .../chatfilter/JagexPrintableCharMatcher.java | 39 ++++ .../src/main/scripts/ChatBuilder.rs2asm | 13 +- .../src/main/scripts/ChatSplitBuilder.rs2asm | 13 +- .../chatfilter/ChatFilterPluginTest.java | 113 +++++++++ 7 files changed, 499 insertions(+), 2 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterType.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/JagexPrintableCharMatcher.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/chatfilter/ChatFilterPluginTest.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterConfig.java new file mode 100644 index 0000000000..abc1747b4a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterConfig.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018, Magic fTail + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.chatfilter; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("chatfilter") +public interface ChatFilterConfig extends Config +{ + @ConfigItem( + keyName = "filterType", + name = "Filter type", + description = "Configures how the messages are filtered", + position = 1 + ) + default ChatFilterType filterType() + { + return ChatFilterType.CENSOR_WORDS; + } + + @ConfigItem( + keyName = "filteredWords", + name = "Filtered Words", + description = "List of filtered words, separated by commas", + position = 2 + ) + default String filteredWords() + { + return ""; + } + + @ConfigItem( + keyName = "filteredRegex", + name = "Filtered Regex", + description = "List of regular expressions to filter, one per line", + position = 3 + ) + default String filteredRegex() + { + return ""; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterPlugin.java new file mode 100644 index 0000000000..fab8a1f668 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterPlugin.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2018, Magic fTail + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.chatfilter; + +import com.google.common.base.Splitter; +import com.google.inject.Provides; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import javax.inject.Inject; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.Player; +import net.runelite.api.events.ConfigChanged; +import net.runelite.api.events.OverheadTextChanged; +import net.runelite.api.events.ScriptCallbackEvent; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.util.Text; +import org.apache.commons.lang3.StringUtils; + +@PluginDescriptor( + name = "Chat Filter", + description = "Censor user configurable words or patterns from chat", + enabledByDefault = false +) +public class ChatFilterPlugin extends Plugin +{ + private static final Splitter NEWLINE_SPLITTER = Splitter + .on("\n") + .omitEmptyStrings() + .trimResults(); + + private static final String CENSOR_MESSAGE = "Hey, everyone, I just tried to say something very silly!"; + + private final JagexPrintableCharMatcher jagexPrintableCharMatcher = new JagexPrintableCharMatcher(); + private final List filteredPatterns = new ArrayList<>(); + + @Inject + private Client client; + + @Inject + private ChatFilterConfig config; + + @Provides + ChatFilterConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(ChatFilterConfig.class); + } + + @Override + protected void startUp() throws Exception + { + updateFilteredPatterns(); + } + + @Override + protected void shutDown() throws Exception + { + filteredPatterns.clear(); + } + + @Subscribe + public void onScriptCallbackEvent(ScriptCallbackEvent event) + { + if (!"chatFilterCheck".equals(event.getEventName())) + { + return; + } + + int[] intStack = client.getIntStack(); + int intStackSize = client.getIntStackSize(); + ChatMessageType chatMessageType = ChatMessageType.of(intStack[intStackSize - 1]); + + // Only filter public chat and private messages + switch (chatMessageType) + { + case PUBLIC: + case PUBLIC_MOD: + case AUTOCHAT: + case PRIVATE_MESSAGE_RECEIVED: + case PRIVATE_MESSAGE_RECEIVED_MOD: + case CLANCHAT: + break; + default: + return; + } + + String[] stringStack = client.getStringStack(); + int stringStackSize = client.getStringStackSize(); + + String message = stringStack[stringStackSize - 1]; + String censoredMessage = censorMessage(message); + + if (censoredMessage == null) + { + // Block the message + intStack[intStackSize - 2] = 0; + } + else + { + // Replace the message + stringStack[stringStackSize - 1] = censoredMessage; + } + } + + @Subscribe + public void onOverheadTextChanged(OverheadTextChanged event) + { + if (!(event.getActor() instanceof Player)) + { + return; + } + + String message = censorMessage(event.getOverheadText()); + + event.getActor().setOverheadText(message); + } + + String censorMessage(final String message) + { + String strippedMessage = jagexPrintableCharMatcher.retainFrom(message) + .replace('\u00A0', ' '); + boolean filtered = false; + for (Pattern pattern : filteredPatterns) + { + Matcher m = pattern.matcher(strippedMessage); + + StringBuffer sb = new StringBuffer(); + + while (m.find()) + { + switch (config.filterType()) + { + case CENSOR_WORDS: + m.appendReplacement(sb, StringUtils.repeat("*", m.group(0).length())); + filtered = true; + break; + case CENSOR_MESSAGE: + return CENSOR_MESSAGE; + case REMOVE_MESSAGE: + return null; + } + } + m.appendTail(sb); + + strippedMessage = sb.toString(); + } + + return filtered ? strippedMessage : message; + } + + void updateFilteredPatterns() + { + filteredPatterns.clear(); + + Text.fromCSV(config.filteredWords()).stream() + .map(s -> Pattern.compile(Pattern.quote(s), Pattern.CASE_INSENSITIVE)) + .forEach(filteredPatterns::add); + + NEWLINE_SPLITTER.splitToList(config.filteredRegex()).stream() + .map(s -> + { + try + { + return Pattern.compile(s, Pattern.CASE_INSENSITIVE); + } + catch (PatternSyntaxException ex) + { + return null; + } + }) + .filter(Objects::nonNull) + .forEach(filteredPatterns::add); + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (!"chatfilter".equals(event.getGroup())) + { + return; + } + + updateFilteredPatterns(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterType.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterType.java new file mode 100644 index 0000000000..cf52fa969f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterType.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2018, Magic fTail + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.chatfilter; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum ChatFilterType +{ + CENSOR_WORDS("Censor words"), + CENSOR_MESSAGE("Censor message"), + REMOVE_MESSAGE("Remove message"); + + private final String name; + + @Override + public String toString() + { + return name; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/JagexPrintableCharMatcher.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/JagexPrintableCharMatcher.java new file mode 100644 index 0000000000..53b76cbc19 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/JagexPrintableCharMatcher.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.chatfilter; + +import com.google.common.base.CharMatcher; + +class JagexPrintableCharMatcher extends CharMatcher +{ + @Override + public boolean matches(char c) + { + // Characters which are printable + return (c >= 32 && c <= 126) + || c == 128 + || (c >= 161 && c <= 255); + } +} diff --git a/runelite-client/src/main/scripts/ChatBuilder.rs2asm b/runelite-client/src/main/scripts/ChatBuilder.rs2asm index ccc78a7195..c30307da67 100644 --- a/runelite-client/src/main/scripts/ChatBuilder.rs2asm +++ b/runelite-client/src/main/scripts/ChatBuilder.rs2asm @@ -183,7 +183,18 @@ LABEL157: iload 12 invoke 193 iconst 1 - if_icmpeq LABEL172 + if_icmpeq CHAT_FILTER ; Jump to our new label instead + jump LABEL641 +CHAT_FILTER: + sload 11 ; Load the message + iconst 1 ; Gets changed to 0 if message is blocked + iload 10 ; Load the messageType + sconst "chatFilterCheck" + runelite_callback + pop_int ; Pop the messageType + iconst 1 ; 2nd half of conditional + sstore 11 ; Override the message with our filtered message + if_icmpeq LABEL172 ; Check if we are building this message jump LABEL641 LABEL172: iload 10 diff --git a/runelite-client/src/main/scripts/ChatSplitBuilder.rs2asm b/runelite-client/src/main/scripts/ChatSplitBuilder.rs2asm index 8c48e849dd..51fa25b373 100644 --- a/runelite-client/src/main/scripts/ChatSplitBuilder.rs2asm +++ b/runelite-client/src/main/scripts/ChatSplitBuilder.rs2asm @@ -353,7 +353,18 @@ LABEL311: iload 14 invoke 91 iconst 1 - if_icmpeq LABEL327 + if_icmpeq CHAT_FILTER ; Jump to our new label instead + jump LABEL475 +CHAT_FILTER: + sload 0 ; Load the message + iconst 1 ; Gets changed to 0 if message is blocked + iload 15 ; Load the messageType + sconst "chatFilterCheck" + runelite_callback + pop_int ; Pop the messageType + iconst 1 ; 2nd half of conditional + sstore 0 ; Override the message with our filtered message + if_icmpeq LABEL327 ; Check if we are building this message jump LABEL475 LABEL327: iload 15 diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/chatfilter/ChatFilterPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/chatfilter/ChatFilterPluginTest.java new file mode 100644 index 0000000000..2fe4d88e05 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/chatfilter/ChatFilterPluginTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2019, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.chatfilter; + +import com.google.inject.Guice; +import com.google.inject.testing.fieldbinder.Bind; +import com.google.inject.testing.fieldbinder.BoundFieldModule; +import javax.inject.Inject; +import net.runelite.api.Client; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import static org.mockito.Mockito.when; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ChatFilterPluginTest +{ + @Mock + @Bind + private Client client; + + @Mock + @Bind + private ChatFilterConfig chatFilterConfig; + + @Inject + private ChatFilterPlugin chatFilterPlugin; + + @Before + public void before() + { + Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this); + + when(chatFilterConfig.filterType()).thenReturn(ChatFilterType.CENSOR_WORDS); + when(chatFilterConfig.filteredWords()).thenReturn(""); + when(chatFilterConfig.filteredRegex()).thenReturn(""); + } + + @Test + public void testCensorWords() + { + when(chatFilterConfig.filteredWords()).thenReturn("hat"); + + chatFilterPlugin.updateFilteredPatterns(); + assertEquals("w***s up", chatFilterPlugin.censorMessage("whats up")); + } + + @Test + public void testCensorRegex() + { + when(chatFilterConfig.filterType()).thenReturn(ChatFilterType.REMOVE_MESSAGE); + when(chatFilterConfig.filteredRegex()).thenReturn("5[0-9]x2\n("); + + chatFilterPlugin.updateFilteredPatterns(); + assertNull(chatFilterPlugin.censorMessage("55X2 Dicing | Trusted Ranks | Huge Pay Outs!")); + } + + @Test + public void testBrokenRegex() + { + when(chatFilterConfig.filteredRegex()).thenReturn("Test\n)\n73"); + + chatFilterPlugin.updateFilteredPatterns(); + assertEquals("** isn't funny", chatFilterPlugin.censorMessage("73 isn't funny")); + } + + @Test + public void testCaseSensitivity() + { + when(chatFilterConfig.filterType()).thenReturn(ChatFilterType.CENSOR_MESSAGE); + when(chatFilterConfig.filteredWords()).thenReturn("ReGeX!!!"); + + chatFilterPlugin.updateFilteredPatterns(); + assertEquals("Hey, everyone, I just tried to say something very silly!", + chatFilterPlugin.censorMessage("I love regex!!!!!!!!")); + } + + @Test + public void testNonPrintableCharacters() + { + when(chatFilterConfig.filterType()).thenReturn(ChatFilterType.REMOVE_MESSAGE); + when(chatFilterConfig.filteredWords()).thenReturn("test"); + + chatFilterPlugin.updateFilteredPatterns(); + assertNull(chatFilterPlugin.censorMessage("te\u008Cst")); + } +} \ No newline at end of file