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 <Adam@sigterm.info>
This commit is contained in:
Magic fTail
2019-03-18 18:00:35 +01:00
committed by Adam
parent ee98065735
commit b7944bcee4
7 changed files with 499 additions and 2 deletions

View File

@@ -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 "";
}
}

View File

@@ -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<Pattern> 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();
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* 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);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,113 @@
/*
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* 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"));
}
}