diff --git a/pom.xml b/pom.xml index 9844209..d4899f2 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,10 @@ org.bstats org.zeroBzeroT.bstats + + redis.clients + org.zeroBzeroT.chatCo.shaded.redis + @@ -62,6 +66,12 @@ META-INF/*.MF + + redis.clients:* + + META-INF/*.MF + + META-INF/** @@ -100,6 +110,10 @@ https://repo.codemc.org/repository/maven-public/ default + + central + https://repo1.maven.org/maven2/ + @@ -121,5 +135,11 @@ 3.1.0 compile + + redis.clients + jedis + 5.1.3 + compile + \ No newline at end of file diff --git a/src/main/java/org/zeroBzeroT/chatCo/ChatPlayer.java b/src/main/java/org/zeroBzeroT/chatCo/ChatPlayer.java index 487f9ff..48983d4 100644 --- a/src/main/java/org/zeroBzeroT/chatCo/ChatPlayer.java +++ b/src/main/java/org/zeroBzeroT/chatCo/ChatPlayer.java @@ -25,6 +25,8 @@ public class ChatPlayer { private List ignores; private List ignoredBy; + public boolean rulesNoticeSent; + public ChatPlayer(final Player p) throws IOException { name = p.getName(); @@ -34,7 +36,8 @@ public ChatPlayer(final Player p) throws IOException { LastMessenger = null; LastReceiver = null; ignoredBy = new ArrayList<>(); - + rulesNoticeSent = false; + // create the ignore-list saveIgnoreList(""); } diff --git a/src/main/java/org/zeroBzeroT/chatCo/LinkBlocker.java b/src/main/java/org/zeroBzeroT/chatCo/LinkBlocker.java new file mode 100644 index 0000000..37256a5 --- /dev/null +++ b/src/main/java/org/zeroBzeroT/chatCo/LinkBlocker.java @@ -0,0 +1,83 @@ +package org.zeroBzeroT.chatCo; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class LinkBlocker { + + public static final Pattern URL_PATTERN = Pattern.compile( + "https?://(?:www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b[-a-zA-Z0-9()@:%_\\\\+.~#?&/=]*"); + + private Set domainAllowlist = new HashSet<>(); + private String replacement = "[link removed]"; + private boolean loaded = false; + + public synchronized void load(File whitelistFile, String replacement) { + this.replacement = replacement == null ? "[link removed]" : replacement; + this.domainAllowlist = new HashSet<>(); + if (whitelistFile != null && whitelistFile.exists()) { + try (BufferedReader r = new BufferedReader(new FileReader(whitelistFile))) { + String line; + while ((line = r.readLine()) != null) { + String t = line.trim().toLowerCase(Locale.ROOT); + if (t.isEmpty() || t.startsWith("#")) continue; + if (t.startsWith("http://")) t = t.substring(7); + else if (t.startsWith("https://")) t = t.substring(8); + if (t.endsWith("/")) t = t.substring(0, t.length() - 1); + domainAllowlist.add(t); + } + } catch (IOException ignored) {} + } + this.loaded = true; + } + + public synchronized String apply(String message) { + if (!loaded || message == null || message.isEmpty()) return message; + Matcher m = URL_PATTERN.matcher(message); + StringBuilder out = new StringBuilder(message.length()); + int last = 0; + while (m.find()) { + String url = m.group(); + if (isAllowed(url)) { + continue; + } + out.append(message, last, m.start()); + if (!replacement.isEmpty()) { + out.append(replacement); + } else if (out.length() > 0 && out.charAt(out.length() - 1) == ' ') { + out.setLength(out.length() - 1); + } + last = m.end(); + } + out.append(message, last, message.length()); + return out.toString(); + } + + private boolean isAllowed(String url) { + String host = extractHost(url); + if (host == null) return false; + return domainAllowlist.contains(host); + } + + private static String extractHost(String url) { + String s = url.toLowerCase(Locale.ROOT); + if (s.startsWith("https://")) s = s.substring(8); + else if (s.startsWith("http://")) s = s.substring(7); + int slash = s.indexOf('/'); + if (slash >= 0) s = s.substring(0, slash); + int colon = s.indexOf(':'); + if (colon >= 0) s = s.substring(0, colon); + return s.isEmpty() ? null : s; + } + + public boolean isLoaded() { + return loaded; + } +} diff --git a/src/main/java/org/zeroBzeroT/chatCo/Main.java b/src/main/java/org/zeroBzeroT/chatCo/Main.java index aaf2904..4120520 100644 --- a/src/main/java/org/zeroBzeroT/chatCo/Main.java +++ b/src/main/java/org/zeroBzeroT/chatCo/Main.java @@ -6,6 +6,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.UUID; import org.bstats.bukkit.Metrics; import org.bukkit.Bukkit; @@ -26,9 +27,20 @@ public class Main extends JavaPlugin { public static File WhisperLog; public static File dataFolder; private static File Help; + public static File WordListFile; + public static File WhiteListFile; public Collection playerList; private Whispers whispers; - + private WordFilter wordFilter; + private LinkBlocker linkBlocker; + private int thresholdHours; + private PublicChat publicChat; + private static Main instance; + + public static Main getInstance() { + return instance; + } + @Override public void onDisable() { playerList.clear(); @@ -36,6 +48,7 @@ public void onDisable() { @Override public void onEnable() { + instance = this; playerList = Collections.synchronizedCollection(new ArrayList<>()); // Config defaults @@ -44,10 +57,13 @@ public void onEnable() { saveResourceFiles(); toggleConfigValue(0); + loadFilters(); + reloadGateConfig(); final PluginManager pm = getServer().getPluginManager(); - pm.registerEvents(new PublicChat(this), this); + publicChat = new PublicChat(this); + pm.registerEvents(publicChat, this); if (getConfig().getBoolean("ChatCo.whisperChangesEnabled", true)) { whispers = new Whispers(this); @@ -63,6 +79,46 @@ public void onEnable() { } } + public void loadFilters() { + if (wordFilter == null) wordFilter = new WordFilter(); + if (linkBlocker == null) linkBlocker = new LinkBlocker(); + boolean fuzzy = getConfig().getBoolean("ChatCo.playtimeGate.wordFilter.fuzzy", false); + String replacement = getConfig().getString("ChatCo.playtimeGate.wordFilter.replacement", "bobba"); + wordFilter.load(WordListFile, WhiteListFile, fuzzy, replacement); + String linkRepl = getConfig().getString("ChatCo.playtimeGate.linkBlock.replacement", "[link removed]"); + linkBlocker.load(WhiteListFile, linkRepl); + } + + public void reloadGateConfig() { + thresholdHours = getConfig().getInt("ChatCo.playtimeGate.thresholdHours", 10); + } + + public WordFilter getWordFilter() { + return wordFilter; + } + + public LinkBlocker getLinkBlocker() { + return linkBlocker; + } + + public boolean isGated(org.bukkit.entity.Player player) { + if (thresholdHours == 0) return false; + if (thresholdHours < 0) return true; + int hours = getPlaytimeHours(player.getUniqueId()); + return hours < thresholdHours; + } + + private int getPlaytimeHours(UUID uuid) { + String key = "player." + uuid + ".playtime"; + java.util.Optional value = Redis.readSync(key); + if (value.isEmpty()) return 0; + try { + return Integer.parseInt(value.get()); + } catch (NumberFormatException e) { + return 0; + } + } + private void toggleConfigValue(final int change) { switch (change) { case 3: @@ -101,6 +157,8 @@ private void saveResourceFiles() { Main.PermissionConfig = new File(Main.dataFolder, "permissionConfig.yml"); Main.WhisperLog = new File(Main.dataFolder, "whisperlog.txt"); Main.Help = new File(Main.dataFolder, "help.txt"); + Main.WordListFile = new File(Main.dataFolder, "wordlist.txt"); + Main.WhiteListFile = new File(Main.dataFolder, "whitelist.txt"); if (!Main.WhisperLog.exists()) { Main.WhisperLog.getParentFile().mkdirs(); @@ -112,6 +170,16 @@ private void saveResourceFiles() { saveStreamToFile(getResource("help.txt"), Main.Help); } + if (!Main.WordListFile.exists()) { + Main.WordListFile.getParentFile().mkdirs(); + saveStreamToFile(getResource("wordlist.txt"), Main.WordListFile); + } + + if (!Main.WhiteListFile.exists()) { + Main.WhiteListFile.getParentFile().mkdirs(); + saveStreamToFile(getResource("whitelist.txt"), Main.WhiteListFile); + } + // Save the default config file, if it does not exist saveDefaultConfig(); @@ -203,6 +271,9 @@ public boolean onCommand(final @NotNull CommandSender sender, final @NotNull Com if (args.length > 0 && args[0].equalsIgnoreCase("reload")) { reloadConfig(); saveConfig(); + loadFilters(); + reloadGateConfig(); + if (publicChat != null) publicChat.reload(); sender.sendMessage("Config reloaded"); return true; } diff --git a/src/main/java/org/zeroBzeroT/chatCo/PublicChat.java b/src/main/java/org/zeroBzeroT/chatCo/PublicChat.java index b744ff4..b4ae326 100644 --- a/src/main/java/org/zeroBzeroT/chatCo/PublicChat.java +++ b/src/main/java/org/zeroBzeroT/chatCo/PublicChat.java @@ -21,11 +21,20 @@ import org.bukkit.event.player.PlayerQuitEvent; import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.regex.Pattern; public class PublicChat implements Listener { public final Main plugin; private final FileConfiguration permissionConfig; + private volatile Map chatPrefixes = Collections.emptyMap(); + private volatile List chatInlineColors = Collections.emptyList(); + + private record InlineColor(String configKey, Pattern pattern, int triggerLength, NamedTextColor color) {} public static final Pattern DEFAULT_URL_PATTERN = Pattern.compile("https?://(?:www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b[-a-zA-Z0-9()@:%_\\\\+.~#?&/=]*"); @@ -33,16 +42,45 @@ public PublicChat(final Main plugin) { this.plugin = plugin; File customConfig = Main.PermissionConfig; permissionConfig = YamlConfiguration.loadConfiguration(customConfig); + reload(); + } + + public void reload() { + Map prefixes = new HashMap<>(); + List inlines = new ArrayList<>(); + + for (String color : NamedTextColor.NAMES.keys()) { + String prefixKey = "ChatCo.chatPrefixes." + color; + String prefixValue = plugin.getConfig().getString(prefixKey); + if (prefixValue != null) { + prefixes.put(color, prefixValue); + } + + String inlineKey = "ChatCo.chatColors." + color; + String inlineValue = plugin.getConfig().getString(inlineKey); + if (inlineValue != null) { + inlines.add(new InlineColor( + inlineKey, + Pattern.compile(Pattern.quote(inlineValue) + ".*$"), + inlineValue.length(), + NamedTextColor.NAMES.value(color) + )); + } + } + + this.chatPrefixes = prefixes; + this.chatInlineColors = inlines; } public Component replacePrefixColors(Component message, final Player player) { String messagePlain = PlainTextComponentSerializer.plainText().serialize(message); - for (String color : NamedTextColor.NAMES.keys()) { + for (Map.Entry entry : chatPrefixes.entrySet()) { + String color = entry.getKey(); + String configValue = entry.getValue(); String configKey = "ChatCo.chatPrefixes." + color; - String configValue = plugin.getConfig().getString(configKey); - if (configValue != null && messagePlain.startsWith(configValue)) { + if (messagePlain.startsWith(configValue)) { if (permissionConfig.getBoolean(configKey, false) || player.hasPermission(configKey)) { return message.color(NamedTextColor.NAMES.value(color)); } @@ -53,17 +91,14 @@ public Component replacePrefixColors(Component message, final Player player) { } public Component replaceInlineColors(Component message, final Player player) { - for (String color : NamedTextColor.NAMES.keys()) { - String configKey = "ChatCo.chatColors." + color; - String configValue = plugin.getConfig().getString(configKey); - - if (configValue != null) { - if (permissionConfig.getBoolean(configKey, false) || player.hasPermission(configKey)) { - return message.replaceText(TextReplacementConfig.builder() - .match(Pattern.quote(configValue) + ".*$") - .replacement(s -> s.content(s.content().substring(configValue.length())).color(NamedTextColor.NAMES.value(color))) - .build()); - } + for (InlineColor ic : chatInlineColors) { + if (permissionConfig.getBoolean(ic.configKey(), false) || player.hasPermission(ic.configKey())) { + final NamedTextColor namedColor = ic.color(); + final int triggerLen = ic.triggerLength(); + return message.replaceText(TextReplacementConfig.builder() + .match(ic.pattern()) + .replacement(s -> s.content(s.content().substring(triggerLen)).color(namedColor)) + .build()); } } @@ -83,6 +118,14 @@ private Component replaceUrls(Component component) { ); } + private Component buildMessage(Player senderPlayer, Component sender, String text) { + Component messageText = Component.text(text); + messageText = replacePrefixColors(messageText, senderPlayer); + messageText = replaceInlineColors(messageText, senderPlayer); + messageText = replaceUrls(messageText); + return Component.text("").append(Component.text("<")).append(sender).append(Component.text("> ")).append(messageText); + } + /** * See Text (Chat Components) */ @@ -96,20 +139,30 @@ public void onAsyncChat(AsyncChatEvent event) { return; } - // Message text - Component messageText = Component.text(legacyMessage); - // Player final Player player = event.getPlayer(); + final boolean gated = plugin.isGated(player); - // Replace color codes - messageText = replacePrefixColors(messageText, player); - messageText = replaceInlineColors(messageText, player); + // Apply gate filters (word filter + link block) to the outgoing text + if (gated) { + if (plugin.getConfig().getBoolean("ChatCo.playtimeGate.wordFilter.enabled", true) + && plugin.getWordFilter() != null && plugin.getWordFilter().isLoaded()) { + legacyMessage = plugin.getWordFilter().apply(legacyMessage); + } - // Clickable links - messageText = replaceUrls(messageText); + if (plugin.getConfig().getBoolean("ChatCo.playtimeGate.linkBlock.enabled", true) + && plugin.getLinkBlocker() != null && plugin.getLinkBlocker().isLoaded()) { + legacyMessage = plugin.getLinkBlocker().apply(legacyMessage); + } + } + + // If filtering emptied the message, drop it entirely + if (gated && legacyMessage.trim().isEmpty()) { + event.viewers().clear(); + event.setCancelled(true); + return; + } - // Sender name Component sender = player.displayName(); if (plugin.getConfig().getBoolean("ChatCo.whisperOnClick", true)) { @@ -117,10 +170,7 @@ public void onAsyncChat(AsyncChatEvent event) { sender = sender.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Whisper to " + player.getName()))); } - // Build Message - TextComponent message = Component.text("").append(Component.text("<")).append(sender).append(Component.text("> ")).append(messageText); - - // Send to the players + // Send to the players, per-recipient so gated viewers can see a filtered version if (!plugin.getConfig().getBoolean("ChatCo.chatDisabled", false)) { for (Audience recipient : event.viewers()) { try { @@ -133,13 +183,43 @@ public void onAsyncChat(AsyncChatEvent event) { continue; } - recipient.sendMessage(message); + // per recipient gate: gated viewers get the sanitized text, others get the original + String recipientText = legacyMessage; + if (recipient instanceof Player && plugin.isGated((Player) recipient)) { + if (plugin.getConfig().getBoolean("ChatCo.playtimeGate.wordFilter.enabled", true) + && plugin.getWordFilter() != null && plugin.getWordFilter().isLoaded()) { + recipientText = plugin.getWordFilter().apply(recipientText); + } + if (plugin.getConfig().getBoolean("ChatCo.playtimeGate.linkBlock.enabled", true) + && plugin.getLinkBlocker() != null && plugin.getLinkBlocker().isLoaded()) { + recipientText = plugin.getLinkBlocker().apply(recipientText); + } + if (recipientText.trim().isEmpty()) continue; + } + + Component recipientMessage = buildMessage(player, sender, recipientText); + recipient.sendMessage(recipientMessage); } catch (NullPointerException e) { e.printStackTrace(); } } } + // 1 time rules notice for gated senders (do we need to store it in Redis so it persists across restart? probably not) + if (gated + && plugin.getConfig().getBoolean("ChatCo.playtimeGate.rulesNotice.enabled", true)) { + ChatPlayer chatPlayer = plugin.getChatPlayer(player); + if (!chatPlayer.rulesNoticeSent) { + String rulesMsg = plugin.getConfig().getString("ChatCo.playtimeGate.rulesNotice.message"); + if (rulesMsg != null && !rulesMsg.isEmpty()) { + for (String line : rulesMsg.split("\\n")) { + player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(line)); + } + } + chatPlayer.rulesNoticeSent = true; + } + } + // Do not send it to the players again - no event cancelling, so that other plugins can process the chat event.viewers().clear(); } diff --git a/src/main/java/org/zeroBzeroT/chatCo/Redis.java b/src/main/java/org/zeroBzeroT/chatCo/Redis.java new file mode 100644 index 0000000..9d3dcdf --- /dev/null +++ b/src/main/java/org/zeroBzeroT/chatCo/Redis.java @@ -0,0 +1,50 @@ +package org.zeroBzeroT.chatCo; + +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class Redis { + + private static boolean warnedDown = false; + + private static HostAndPort getHost() { + return new HostAndPort( + Main.getInstance().getConfig().getString("ChatCo.redis.host", "127.0.0.1"), + Main.getInstance().getConfig().getInt("ChatCo.redis.port", 6379)); + } + + private static Optional read(String key) { + final HostAndPort host = getHost(); + final Jedis jedis; + final String value; + try { + jedis = new Jedis(host); + value = jedis.get(key); + } catch (JedisConnectionException e) { + if (!warnedDown) { + warnedDown = true; + Bukkit.getLogger().warning("[ChatCo] Redis not reachable at " + + host.getHost() + ":" + host.getPort() + + " (" + e.getMessage() + "). Gating will fail closed."); + } + return Optional.empty(); + } + jedis.close(); + if (value == null) return Optional.empty(); + return Optional.of(value); + } + + public static CompletableFuture> readAsync(String key) { + return CompletableFuture.supplyAsync(() -> read(key)); + } + + public static Optional readSync(String key) { + return read(key); + } +} diff --git a/src/main/java/org/zeroBzeroT/chatCo/WordFilter.java b/src/main/java/org/zeroBzeroT/chatCo/WordFilter.java new file mode 100644 index 0000000..fd0fdb1 --- /dev/null +++ b/src/main/java/org/zeroBzeroT/chatCo/WordFilter.java @@ -0,0 +1,161 @@ +package org.zeroBzeroT.chatCo; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +public class WordFilter { + + private Set words = new HashSet<>(); + private Set whitelist = new HashSet<>(); + private int minWordLength = Integer.MAX_VALUE; + private boolean fuzzy = false; + private String replacement = "bobba"; + private boolean loaded = false; + + public synchronized void load(File wordlist, File whitelistFile, boolean fuzzy, String replacement) { + this.fuzzy = fuzzy; + this.replacement = replacement == null ? "bobba" : replacement; + this.words = readSet(wordlist); + this.whitelist = readSet(whitelistFile); + this.minWordLength = Integer.MAX_VALUE; + for (String w : words) { + if (w.length() < minWordLength) minWordLength = w.length(); + } + this.loaded = true; + } + + public synchronized String apply(String message) { + if (!loaded || words.isEmpty() || message == null || message.isEmpty()) { + return message; + } + + StringBuilder out = new StringBuilder(message); + String normalized = leetNormalize(message.toLowerCase(Locale.ROOT)); + + int n = normalized.length(); + int i = 0; + while (i < n) { + while (i < n && !isLetter(normalized.charAt(i))) i++; + if (i >= n) break; + int start = i; + while (i < n && isLetter(normalized.charAt(i))) i++; + int tokenLen = i - start; + if (tokenLen < minWordLength) continue; + if (isMatch(normalized, start, tokenLen)) { + int end = i; + if (replacement.isEmpty()) { + int clearEnd = end; + if (clearEnd < out.length() && out.charAt(clearEnd) == ' ') { + clearEnd++; + } + for (int k = start; k < clearEnd; k++) { + out.setCharAt(k, ' '); + } + } else { + for (int k = start; k < end; k++) { + out.setCharAt(k, replacement.charAt(Math.min(k - start, replacement.length() - 1))); + } + } + } + } + return out.toString(); + } + + private boolean isMatch(String normalized, int start, int len) { + for (String wl : whitelist) { + if (wl.length() == len && regionMatchesIgnoreCase(normalized, start, wl, 0, len)) { + return false; + } + } + for (String w : words) { + if (w.length() != len) continue; + if (regionMatchesIgnoreCase(normalized, start, w, 0, len)) return true; + } + if (!fuzzy) return false; + if (len < 4) return false; + for (String w : words) { + if (Math.abs(w.length() - len) > 1) continue; + if (levenRegion(normalized, start, len, w)) return true; + } + return false; + } + + private static boolean regionMatchesIgnoreCase(String s, int sOff, String other, int oOff, int len) { + for (int k = 0; k < len; k++) { + char a = s.charAt(sOff + k); + char b = other.charAt(oOff + k); + if (a == b) continue; + char al = (a >= 'A' && a <= 'Z') ? (char) (a + 32) : a; + char bl = (b >= 'A' && b <= 'Z') ? (char) (b + 32) : b; + if (al != bl) return false; + } + return true; + } + + private static boolean isLetter(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + private static String leetNormalize(String s) { + StringBuilder b = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '0': b.append('o'); break; + case '1': b.append('i'); break; + case '3': b.append('e'); break; + case '4': b.append('a'); break; + case '5': b.append('s'); break; + case '7': b.append('t'); break; + case '@': b.append('a'); break; + case '$': b.append('s'); break; + default: b.append(c); + } + } + return b.toString(); + } + + private static boolean levenRegion(String s, int sOff, int sLen, String t) { + int tLen = t.length(); + int[] prev = new int[tLen + 1]; + int[] curr = new int[tLen + 1]; + for (int j = 0; j <= tLen; j++) prev[j] = j; + for (int i = 1; i <= sLen; i++) { + curr[0] = i; + char sc = (s.charAt(sOff + i - 1) >= 'A' && s.charAt(sOff + i - 1) <= 'Z') + ? (char) (s.charAt(sOff + i - 1) + 32) : s.charAt(sOff + i - 1); + for (int j = 1; j <= tLen; j++) { + char tc = t.charAt(j - 1); + int cost = sc == tc ? 0 : 1; + curr[j] = Math.min(Math.min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); + } + int[] tmp = prev; prev = curr; curr = tmp; + } + return prev[tLen] <= 1; + } + + private static Set readSet(File f) { + Set out = new HashSet<>(); + if (f == null || !f.exists()) return out; + try (BufferedReader r = new BufferedReader(new FileReader(f))) { + String line; + while ((line = r.readLine()) != null) { + String t = line.trim().toLowerCase(Locale.ROOT); + if (t.isEmpty() || t.startsWith("#")) continue; + out.add(t); + } + } catch (IOException e) { + // best-effort + } + return out; + } + + public boolean isLoaded() { + return loaded; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index fe2cd77..aa616f2 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -63,4 +63,28 @@ ChatCo: strikethrough: null underline: null white: null - yellow: null \ No newline at end of file + yellow: null + + redis: + host: 127.0.0.1 + port: 6379 + + playtimeGate: + thresholdHours: 10 + + wordFilter: + enabled: true + # set to "" to delete the matched word entirely + replacement: "*" + fuzzy: false + + rulesNotice: + enabled: true + message: |- + &6Please follow the community standards: + &9https://www.minecraft.net/en-us/community-standards + + linkBlock: + enabled: true + # set to "" to delete the URL entirely + replacement: "[link removed]" \ No newline at end of file diff --git a/src/main/resources/whitelist.txt b/src/main/resources/whitelist.txt new file mode 100644 index 0000000..9078637 --- /dev/null +++ b/src/main/resources/whitelist.txt @@ -0,0 +1,2 @@ +example.com +minecraft.net diff --git a/src/main/resources/wordlist.txt b/src/main/resources/wordlist.txt new file mode 100644 index 0000000..f6ac19d --- /dev/null +++ b/src/main/resources/wordlist.txt @@ -0,0 +1,772 @@ +# minecraft bedrock edition's block list https://github.com/The-phoenixR/minecraft-profanity/tree/main +1488 +8=D +A55hole +abortion +ahole +AIDS +AIDs +ainujin +ainuzin +akimekura +Anal +anus +anuses +Anushead +anuslick +anuss +aokan +Arsch +Arschloch +arse +arsed +arsehole +arseholed +arseholes +arseholing +arselicker +arses +Ass +asshat +asshole +assholed +assholes +assholing +asslick +asslicker +asses +Auschwitz +b00bs +b00bz +b1tc +Baise +bakachon +bakatyon +Ballsack +Ballzack +BAMF +Bastard +Beaner +Beeatch +beeeyotch +beefwhistle +beeotch +Beetch +beeyotch +Bellend +bestiality +beyitch +beyotch +Biach +bin laden +binladen +biotch +bitch +bitches +Bitching +blad +bladt +blowjob +blow job +blowme +blow me +blyad +blyadt +bon3r +boner +boobs +boobz +Btch +Bukakke +Bullshit +butagorosi +butthead +Butthole +Buttplug +c0ck +Cabron +Cacca +Cadela +Cagada +Cameljockey +Caralho +castrate +Cazzo +ceemen +ch1nk +chankoro +chieokure +chikusatsu +Ching chong +Chinga +Chingada Madre +Chingado +Chingate +chink +chinpo +Chlamydia +choad +chode +chonga +chonko +chonkoro +chourimbo +chourinbo +chourippo +chuurembo +chuurenbo +circlejerk +cl1t +cli7 +clit +clitoris +cocain +Cocaine +cock +Cocksucker +Coglione +Coglioni +coitus +coituss +cojelon +cojones +condom +coon +coon hunt +coon kill +coonhunt +coonkill +Cooter +cotton pic +cotton pik +cottonpic +cottonpik +Crackhead +crap +CSAM +Culear +Culero +Culo +Cum +cumming +cun7 +cunt +cvn7 +cvnt +cyka +d1kc +d4go +dago +Darkie +Deez Nuts +Deez Nutz +deeznut +deeznuts +deeznutz +Dickhead +dickwad +dick +dicks +dikc +dildo +Dio Bestia +dong +dongs +douche +Downie +Dumass +Dumbass +Durka durka +Dyke +Ejaculate +Encule +enjokousai +enzyokousai +etahinin +etambo +etanbo +f0ck +f0kc +f3lch +facking +fag +faggot +fags +faggots +Fanculo +Fanny +fatass +fck +Fckn +fcuk +fcuuk +felch +fellatio +Fetish +Fgt +FiCKDiCH +Figlio di Puttana +fku +fock +fokc +foreskin +Fotze +Foutre +fucc +fuck +fucks +fuckd +fucked +fucker +fuckers +fucking +fuckr +fucky +fuct +fujinoyamai +fukashokumin +Fupa +fuuck +fuuckd +fuucked +fuucker +fuucking +fuuckr +fuuuck +fuuuckd +fuuucked +fuuucker +fuuucking +fuuuckr +fuuuuck +fuuuuckd +fuuuucked +fuuuucker +fuuuucking +fuuuuckr +fuuuuuck +fuuuuuckd +fuuuuucked +fuuuuucker +fuuuuucking +fuuuuuckr +fuuuuuuck +fuuuuuuckd +fuuuuuucked +fuuuuuucker +fuuuuuucking +fuuuuuuckr +fuuuuuuuck +fuuuuuuuckd +fuuuuuuucked +fuuuuuuucker +fuuuuuuucking +fuuuuuuuckr +fuuuuuuuuck +fuuuuuuuuckd +fuuuuuuuucked +fuuuuuuuucker +fuuuuuuuucking +fuuuuuuuuckr +fuuuuuuuuuck +fuuuuuuuuuckd +fuuuuuuuuucked +fuuuuuuuuucker +fuuuuuuuuucking +fuuuuuuuuuckr +fuuuuuuuuuu +fvck +fxck +fxuxcxk +g000k +g00k +g0ok +gestapo +go0k +god damn +goddamn +goldenshowers +golliwogg +gollywog +Gooch +gook +goook +Gyp +h0m0 +h0mo +h1tl3 +h1tle +hairpie +hakujakusha +hakuroubyo +hakuzyakusya +hantoujin +hantouzin +Herpes +hitl3r +hitler +hitlr +holocaust +hom0 +homo +honky +Hooker +hor3 +hore +hukasyokumin +Hurensohn +huzinoyamai +hymen +inc3st +incest +Inculato +Injun +intercourse +inugoroshi +inugorosi +j1g4b0 +j1g4bo +j1gab0 +j1gabo +Jack Off +jackass +jap +JerkOff +jig4b0 +jig4bo +jigabo +Jigaboo +jiggaboo +jizz +Joder +Joto +Jungle Bunny +junglebunny +k k k +k1k3 +kichigai +kik3 +Kike +kikeiji +kikeizi +Kilurself +kitigai +kkk +klu klux +Klu Klux Klan +kluklux +knobhead +koon hunt +koon kill +koonhunt +koonkill +koroshiteyaru +koumoujin +koumouzin +ku klux klan +kun7 +kurombo +Kurva +Kurwa +kxkxk +l3sb0 +lesbo +lezbo +lezzie +m07th3rfukr +m0th3rfvk3r +m0th3rfvker +Madonna Puttana +manberries +manko +manshaft +Maricon +Masterbat +masterbate +Masturbacion +masturbait +Masturbare +Masturbate +Masturbazione +Merda +Merde +Meth +Mierda +milf +Minge +Miststück +mitsukuchi +mitukuti +Molest +molester +molestor +Mong +Moon Cricket +moth3rfucer +moth3rfvk3r +moth3rfvker +motherfucker +Mulatto +n1663r +n1664 +n166a +n166er +n1g3r +n1German +n1gg3r +n1gGerman +n3gro +n4g3r +n4gg3r +n4gGerman +n4z1 +nag3r +nagg3r +nagGerman +natzi +naz1 +nazi +nazl +neGerman +ngGerman +nggr +NhigGerman +ni666 +ni66a +ni66er +ni66g +ni6g +ni6g6 +ni6gg +Nig +nig66 +nig6g +nigar +niGerman +nigg3 +nigg6 +nigga +niggaz +nigGerman +nigger +niggers +nigglet +niggr +nigguh +niggur +niggy +niglet +Nignog +nimpinin +ninpinin +Nipples +niqqa +niqqer +Nonce +nugga +Nutsack +Nutted +nygGerman +omeko +Orgy +p3n15 +p3n1s +p3ni5 +p3nis +p3nl5 +p3nls +Paki +Panties +Pedo +pedoph +pedophile +pen15 +pen1s +Pendejo +peni5 +penile +penis +Penis +penl5 +penls +penus +Perra +phag +phaggot +phagot +phuck +Pikey +Pinche +Pizda +Polla +Porca Madonna +Porch monkey +Porn +Pornhub +Porra +pr1ck +preteen +prick +pu555y +pu55y +pub1c +Pube +pubic +pun4ni +pun4nl +Punal +punan1 +punani +punanl +puss1 +puss3 +puss5 +pusse +pussi +pussy +pussys +Pussies +pusss1 +pussse +pusssi +pusssl +pusssy +Pussy +Puta +Putain +Pute +Puto +Puttana +Puttane +Puttaniere +puzzy +pvssy +queef +r3c7um +r4p15t +r4p1st +r4p3 +r4pi5t +r4pist +raape +raghead +raibyo +Raip +rap15t +rap1st +Rapage +rape +Raped +rapi5t +Raping +rapist +rectum +Red Tube +Reggin +reipu +retard +Ricchione +rimjob +rimming +rizzape +rompari +Salaud +Salope +sangokujin +sangokuzin +santorum +Scheiße +Schlampe +Schlampe +schlong +Schwuchtel +Scrote +secks +seishinhakujaku +seishinijo +seisinhakuzyaku +seisinizyo +Semen +semushiotoko +semusiotoko +sh|t +sh|thead +sh|tstain +sh17 +sh17head +sh17stain +sh1t +sh1thead +sh1tstain +Shat +Shemale +shi7 +shi7head +shi7stain +shinajin +shinheimin +shirakko +shit +shithead +shitstain +shitting +Shitty +shokubutsuningen +sinazin +sinheimin +Skank +SMD +Sodom +sofa king +sofaking +Spanishick +Spanishook +Spanishunk +STD +STDs +Succhia Cazzi +suck my +suckmy +syokubutuningen +Taint +Tapatte +Tapette +Tarlouse +tea bag +teabag +teebag +teensex +teino +Testa di Cazzo +Testicles +Thot +tieokure +tinpo +Tits +Titz +titties +tittiez +tokushugakkyu +tokusyugakkyu +torukoburo +torukojo +torukozyo +tosatsu +tosatu +towelhead +Trannie +Tranny +tunbo +tw47 +tw4t +twat +tyankoro +tyonga +tyonko +tyonkoro +tyourinbo +tyourippo +tyurenbo +ushigoroshi +usigorosi +v461n4 +v461na +v46in4 +v46ina +v4g1n4 +v4g1na +v4gin4 +v4gina +va61n4 +va61na +va6in4 +va6ina +Vaccagare +Vaffanculo +Vag +vag1n4 +vag1na +vagin4 +vagina +VateFaire +vvhitepower +w3tb4ck +w3tback +Wank +wanker +wetb4ck +wetback +wh0r3 +wh0re +white power +whitepower +whor3 +whore +whores +Wog +Wop +x8lp3t +xbl pet +XBLPET +XBLRewards +Xl3LPET +yabunirami +Zipperhead +Блядь +сука +アオカン +あおかん +イヌゴロシ +いぬごろし +インバイ +いんばい +オナニー +おなにー +オメコ +カワラコジキ +かわらこじき +カワラモノ +かわらもの +キケイジ +きけいじ +キチガイ +きちがい +キンタマ +きんたま +クロンボ +くろんぼ +コロシテヤル +ころしてやる +シナジン +しなじん +タチンボ +たちんぼ +チョンコウ +ちょんこう +チョンコロ +ちょんころ +ちょん公 +チンポ +ちんぽ +ツンボ +つんぼ +とるこじょう +とるこぶろ +トルコ嬢 +トルコ風呂 +ニガー +ニグロ +にんぴにん +はんとうじん +マンコ +まんこ +レイプ +れいぷ +低能 +屠殺 +強姦 +援交 +支那人 +精薄 +精薄者 +輪姦 \ No newline at end of file