diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 5416a570..7b11e47f 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -12,7 +12,7 @@ java { dependencies { compileOnly("org.jetbrains:annotations:26.1.0") compileOnly("com.google.code.gson:gson:2.12.1") - compileOnly("com.google.guava:guava:33.4.0-jre") + compileOnly("com.google.guava:guava:${property("guavaVersion")}") } buildConfig { diff --git a/gradle.properties b/gradle.properties index dee1f2d8..7d245362 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,8 @@ pluginVersion=2.1.0 versionChannel=release # dependencies +paperVersion=1.21.11-R0.1-SNAPSHOT cloudVersion=2.0.0-beta.15 langchain4jVersion=1.16.3 jmhVersion=1.37 +guavaVersion=33.4.0-jre diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts index 24b44434..45746db6 100644 --- a/paper/build.gradle.kts +++ b/paper/build.gradle.kts @@ -36,7 +36,7 @@ dependencies { implementation("org.incendo:cloud-paper:${property("cloudVersion")}") implementation("org.incendo:cloud-minecraft-extras:${property("cloudVersion")}") - compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") + compileOnly("io.papermc.paper:paper-api:${property("paperVersion")}") compileOnly("me.clip:placeholderapi:2.12.2") compileOnly("org.geysermc.floodgate:api:2.2.5-SNAPSHOT") compileOnly("fr.xephi:authme:5.7.0-SNAPSHOT") @@ -55,7 +55,7 @@ dependencies { runtimeOnly("org.snakeyaml:snakeyaml-engine:2.7") testImplementation("org.junit.jupiter:junit-jupiter:6.1.1") - testImplementation("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") + testImplementation("io.papermc.paper:paper-api:${property("paperVersion")}") testImplementation("dev.langchain4j:langchain4j-open-ai:${property("langchain4jVersion")}") testImplementation("dev.langchain4j:langchain4j-anthropic:${property("langchain4jVersion")}") testImplementation("dev.langchain4j:langchain4j-http-client-jdk:${property("langchain4jVersion")}") @@ -63,7 +63,7 @@ dependencies { add(jmh.implementationConfigurationName, "org.openjdk.jmh:jmh-core:${property("jmhVersion")}") add(jmh.annotationProcessorConfigurationName, "org.openjdk.jmh:jmh-generator-annprocess:${property("jmhVersion")}") - add(jmh.runtimeOnlyConfigurationName, "com.google.guava:guava:33.4.0-jre") + add(jmh.runtimeOnlyConfigurationName, "com.google.guava:guava:${property("guavaVersion")}") } configurations.named(jmh.implementationConfigurationName) { diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/AdvancedSensitiveWords.java b/paper/src/main/java/io/wdsj/asw/bukkit/AdvancedSensitiveWords.java index b297622c..5cabb189 100644 --- a/paper/src/main/java/io/wdsj/asw/bukkit/AdvancedSensitiveWords.java +++ b/paper/src/main/java/io/wdsj/asw/bukkit/AdvancedSensitiveWords.java @@ -5,11 +5,13 @@ import com.github.Anon8281.universalScheduler.scheduling.tasks.MyScheduledTask; import com.github.houbb.sensitive.word.api.IWordAllow; import com.github.houbb.sensitive.word.api.IWordDeny; +import com.github.houbb.sensitive.word.api.IWordResult; import com.github.houbb.sensitive.word.api.IWordResultCondition; import com.github.houbb.sensitive.word.bs.SensitiveWordBs; import com.github.houbb.sensitive.word.support.allow.WordAllows; import com.github.houbb.sensitive.word.support.check.WordChecks; import com.github.houbb.sensitive.word.support.deny.WordDenys; +import com.github.houbb.sensitive.word.support.result.WordResultHandlers; import com.github.houbb.sensitive.word.support.resultcondition.WordResultConditions; import com.github.houbb.sensitive.word.support.tag.WordTags; import io.wdsj.asw.bukkit.command.AswCommandRegistrar; @@ -44,6 +46,10 @@ import org.bukkit.plugin.java.JavaPlugin; import org.slf4j.Logger; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; + import static io.wdsj.asw.bukkit.util.LoggingUtils.purgeLog; import static io.wdsj.asw.bukkit.util.TimingUtils.resetStatistics; import static io.wdsj.asw.bukkit.util.Utils.*; @@ -52,6 +58,7 @@ public final class AdvancedSensitiveWords extends JavaPlugin { public static volatile boolean isInitialized = false; public static SensitiveWordBs sensitiveWordBs; + public static SensitiveWordBs networkSensitiveWordBs; public static boolean isAuthMeAvailable; public static final String PLUGIN_VERSION = PluginBuildInfo.VERSION; private static AdvancedSensitiveWords instance; @@ -134,6 +141,7 @@ public void doInitTasks() { IWordAllow wA = WordAllows.chains(WordAllows.defaults(), new WordAllow(), new ExternalWordAllow(this)); isInitialized = false; sensitiveWordBs = null; + networkSensitiveWordBs = null; IWordResultCondition condition = createWordResultCondition(); getScheduler().runTaskAsynchronously(() -> { IWordDeny wordDeny = createWordDeny(); @@ -145,20 +153,20 @@ public void doInitTasks() { .ignoreEnglishStyle(configurationService.get(PluginSettings.IGNORE_ENGLISH_STYLE)) .ignoreRepeat(configurationService.get(PluginSettings.IGNORE_REPEAT)) .enableNumCheck(configurationService.get(PluginSettings.ENABLE_NUM_CHECK)) - .enableEmailCheck(configurationService.get(PluginSettings.ENABLE_EMAIL_CHECK)) - .enableUrlCheck(configurationService.get(PluginSettings.ENABLE_URL_CHECK)) + .enableEmailCheck(false) + .enableUrlCheck(false) .enableWordCheck(configurationService.get(PluginSettings.ENABLE_WORD_CHECK)) .wordResultCondition(condition) - .wordCheckUrl(configurationService.get(PluginSettings.URL_CHECK_NO_PREFIX) ? WordChecks.urlNoPrefix() : WordChecks.url()) .wordDeny(wordDeny) .wordAllow(wA) .numCheckLen(configurationService.get(PluginSettings.NUM_CHECK_LEN)) .wordReplace(new WordReplace()) .wordTag(WordTags.none()) - .charIgnore(new CharIgnore()) - .enableIpv4Check(configurationService.get(PluginSettings.ENABLE_IP_CHECK)) + .charIgnore(new CharIgnore(configurationService.get(PluginSettings.IGNORE_CHAR))) + .enableIpv4Check(false) .wordFailFast(configurationService.get(PluginSettings.FAIL_FAST)) .init(); + networkSensitiveWordBs = createNetworkSensitiveWordBs(); isInitialized = true; }); } @@ -178,7 +186,12 @@ public void onDisable() { ViolationCounter.INSTANCE.resetAllViolations(); SchedulingUtils.cancelTaskSafely(violationResetTask); if (permCache != null) permCache.disable(); - if (isInitialized) sensitiveWordBs.destroy(); + if (isInitialized) { + sensitiveWordBs.destroy(); + if (networkSensitiveWordBs != null) { + networkSensitiveWordBs.destroy(); + } + } commandRegistrar = null; LOGGER.info("AdvancedSensitiveWords is disabled."); } @@ -282,6 +295,34 @@ private IWordResultCondition createWordResultCondition() { }; } + private SensitiveWordBs createNetworkSensitiveWordBs() { + boolean enableEmail = configurationService.get(PluginSettings.ENABLE_EMAIL_CHECK); + boolean enableUrl = configurationService.get(PluginSettings.ENABLE_URL_CHECK); + boolean enableIp = configurationService.get(PluginSettings.ENABLE_IP_CHECK); + if (!enableEmail && !enableUrl && !enableIp) { + return null; + } + return SensitiveWordBs.newInstance() + .ignoreCase(configurationService.get(PluginSettings.IGNORE_CASE)) + .ignoreWidth(configurationService.get(PluginSettings.IGNORE_WIDTH)) + .ignoreNumStyle(configurationService.get(PluginSettings.IGNORE_NUM_STYLE)) + .ignoreChineseStyle(configurationService.get(PluginSettings.IGNORE_CHINESE_STYLE)) + .ignoreEnglishStyle(configurationService.get(PluginSettings.IGNORE_ENGLISH_STYLE)) + .ignoreRepeat(configurationService.get(PluginSettings.IGNORE_REPEAT)) + .enableNumCheck(false) + .enableEmailCheck(enableEmail) + .enableUrlCheck(enableUrl) + .enableWordCheck(false) + .wordResultCondition(WordResultConditions.alwaysTrue()) + .wordCheckUrl(configurationService.get(PluginSettings.URL_CHECK_NO_PREFIX) ? WordChecks.urlNoPrefix() : WordChecks.url()) + .wordReplace(new WordReplace()) + .wordTag(WordTags.none()) + .charIgnore(new CharIgnore(configurationService.get(PluginSettings.IGNORE_CHAR), CharIgnore.NETWORK_SYNTAX_CHARS)) + .enableIpv4Check(enableIp) + .wordFailFast(configurationService.get(PluginSettings.FAIL_FAST)) + .init(); + } + private IWordDeny createWordDeny() { boolean enableDefaultWords = configurationService.get(PluginSettings.ENABLE_DEFAULT_WORDS); boolean enableOnlineWords = configurationService.get(PluginSettings.ENABLE_ONLINE_WORDS); @@ -297,4 +338,37 @@ private IWordDeny createWordDeny() { return WordDenys.chains(new WordDeny(), new ExternalWordDeny(this)); } + public static List findAllSensitive(String text) { + LinkedHashSet results = new LinkedHashSet<>(); + SensitiveWordBs wordBs = sensitiveWordBs; + if (wordBs != null) { + results.addAll(wordBs.findAll(text)); + } + SensitiveWordBs networkWordBs = networkSensitiveWordBs; + if (networkWordBs != null) { + results.addAll(networkWordBs.findAll(text)); + } + return List.copyOf(results); + } + + public static List findAllSensitiveRaw(String text) { + List results = new ArrayList<>(); + SensitiveWordBs wordBs = sensitiveWordBs; + if (wordBs != null) { + results.addAll(wordBs.findAll(text, WordResultHandlers.raw())); + } + SensitiveWordBs networkWordBs = networkSensitiveWordBs; + if (networkWordBs != null) { + results.addAll(networkWordBs.findAll(text, WordResultHandlers.raw())); + } + return results; + } + + public static String replaceSensitive(String text) { + SensitiveWordBs wordBs = sensitiveWordBs; + String result = wordBs == null ? text : wordBs.replace(text); + SensitiveWordBs networkWordBs = networkSensitiveWordBs; + return networkWordBs == null ? result : networkWordBs.replace(result); + } + } diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/ai/LlmChatDetectionService.java b/paper/src/main/java/io/wdsj/asw/bukkit/ai/LlmChatDetectionService.java index b6b988eb..08aeb49a 100644 --- a/paper/src/main/java/io/wdsj/asw/bukkit/ai/LlmChatDetectionService.java +++ b/paper/src/main/java/io/wdsj/asw/bukkit/ai/LlmChatDetectionService.java @@ -4,14 +4,18 @@ import io.wdsj.asw.bukkit.api.event.AsyncModerationResponseEvent; import io.wdsj.asw.bukkit.api.moderation.LlmChatModerationResult; import io.wdsj.asw.bukkit.api.moderation.LlmModerationCategory; +import io.wdsj.asw.bukkit.permission.option.PlayerOptionView; +import io.wdsj.asw.bukkit.permission.option.PlayerOptions; import io.wdsj.asw.bukkit.setting.PaperConfigurationService; import io.wdsj.asw.bukkit.setting.PluginSettings; +import io.wdsj.asw.bukkit.setting.SettingKey; import io.wdsj.asw.bukkit.type.ModuleType; import io.wdsj.asw.bukkit.util.SchedulingUtils; import io.wdsj.asw.bukkit.util.ViolationReporter; import io.wdsj.asw.common.utils.MessageEntropy; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; +import org.bukkit.entity.Player; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerKickEvent; import org.bukkit.event.player.PlayerQuitEvent; @@ -29,6 +33,7 @@ import java.util.function.Function; import java.nio.charset.StandardCharsets; import io.wdsj.asw.bukkit.setting.SettingsConfiguration; +import org.jetbrains.annotations.VisibleForTesting; public final class LlmChatDetectionService implements Listener, AutoCloseable { private final PaperConfigurationService configuration; @@ -80,28 +85,46 @@ public synchronized void reload() { } } + @VisibleForTesting public void submit(UUID playerId, String playerName, String message) { + submit(playerId, playerName, message, null); + } + + public void submit(Player player, String message, PlayerOptionView options) { + submit(player.getUniqueId(), player.getName(), message, options); + } + + private void submit(UUID playerId, String playerName, String message, PlayerOptionView options) { RuntimeState state = runtime; if (state == null || closed || !AdvancedSensitiveWords.isInitialized || AdvancedSensitiveWords.sensitiveWordBs == null) { return; } - LlmSettings settings = state.settings(); + if (!optionBoolean(options, PlayerOptions.AI_ENABLED, PluginSettings.AI_ENABLED)) { + return; + } int rawCodePoints = message.codePointCount(0, message.length()); - if (rawCodePoints > settings.maximumMessageCodePoints()) { + if (rawCodePoints > optionInt(options, PlayerOptions.AI_MAXIMUM_MESSAGE_CODE_POINTS, PluginSettings.AI_MAXIMUM_MESSAGE_CODE_POINTS)) { return; } int visibleCodePoints = MessageEntropy.visibleCodePointCount(message); - if (visibleCodePoints < settings.minimumMessageCodePoints()) { + if (visibleCodePoints < optionInt(options, PlayerOptions.AI_MINIMUM_MESSAGE_CODE_POINTS, PluginSettings.AI_MINIMUM_MESSAGE_CODE_POINTS)) { return; } double entropy = MessageEntropy.shannonEntropyBits(message); - if (entropy < settings.minimumEntropyBits()) { + if (entropy < optionDouble(options, PlayerOptions.AI_MINIMUM_ENTROPY_BITS, PluginSettings.AI_MINIMUM_ENTROPY_BITS)) { return; } - Candidate candidate = reserveCandidate(state, playerId, playerName, message, entropy); + Candidate candidate = reserveCandidate( + state, + playerId, + playerName, + message, + entropy, + optionInt(options, PlayerOptions.AI_PER_PLAYER_COOLDOWN_SECONDS, PluginSettings.AI_PLAYER_COOLDOWN_SECONDS) + ); if (candidate == null) { droppedRequests.increment(); return; @@ -207,14 +230,15 @@ private Candidate reserveCandidate( UUID playerId, String playerName, String message, - double entropy + double entropy, + int cooldownSeconds ) { if (!isCurrent(state) || inFlightGenerations.putIfAbsent(playerId, state.generation()) != null) { return null; } long now = System.currentTimeMillis(); - long deadline = now + TimeUnit.SECONDS.toMillis(state.settings().perPlayerCooldownSeconds()); + long deadline = now + TimeUnit.SECONDS.toMillis(Math.max(0, cooldownSeconds)); AtomicBoolean granted = new AtomicBoolean(); cooldowns.compute(playerId, (ignored, existing) -> { if (existing != null && existing.deadlineMillis() > now) { @@ -399,6 +423,18 @@ private static Map toCategoryPolicies( return policies; } + private boolean optionBoolean(PlayerOptionView options, String optionPath, SettingKey key) { + return options == null ? configuration.get(key) : options.bool(optionPath, key); + } + + private int optionInt(PlayerOptionView options, String optionPath, SettingKey key) { + return options == null ? configuration.get(key) : options.integer(optionPath, key); + } + + private double optionDouble(PlayerOptionView options, String optionPath, SettingKey key) { + return options == null ? configuration.get(key) : options.decimal(optionPath, key); + } + record LlmSettings( String baseUrl, LlmApiMode apiMode, diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/api/CoreApi.java b/paper/src/main/java/io/wdsj/asw/bukkit/api/CoreApi.java index 39fab786..17a047d5 100644 --- a/paper/src/main/java/io/wdsj/asw/bukkit/api/CoreApi.java +++ b/paper/src/main/java/io/wdsj/asw/bukkit/api/CoreApi.java @@ -28,7 +28,7 @@ public boolean isInitialized() { * @see #isInitialized() */ public List findSensitiveWords(String text) { - return AdvancedSensitiveWords.sensitiveWordBs.findAll(text); + return AdvancedSensitiveWords.findAllSensitive(text); } /** @@ -41,6 +41,6 @@ public List findSensitiveWords(String text) { * @see #isInitialized() */ public String replaceSensitiveWords(String text) { - return AdvancedSensitiveWords.sensitiveWordBs.replace(text); + return AdvancedSensitiveWords.replaceSensitive(text); } } diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/command/AswCommandRegistrar.java b/paper/src/main/java/io/wdsj/asw/bukkit/command/AswCommandRegistrar.java index 8d64690f..dcb9fb6a 100644 --- a/paper/src/main/java/io/wdsj/asw/bukkit/command/AswCommandRegistrar.java +++ b/paper/src/main/java/io/wdsj/asw/bukkit/command/AswCommandRegistrar.java @@ -38,7 +38,7 @@ public final class AswCommandRegistrar { .toList() ); private static final ParserDescriptor VIOLATION_MODULE_PARSER = - StringParser.stringParser().flatMapSuccess(ModuleType.class, (context, value) -> { + StringParser.stringParser().flatMapSuccess(ModuleType.class, (ignored, value) -> { ModuleType moduleType = ModuleType.parseViolationModule(value); if (moduleType == null) { return ArgumentParseResult.failureFuture(new ViolationModuleParseException(value)); @@ -235,4 +235,5 @@ public Throwable fillInStackTrace() { return this; } } + } diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/command/AswCommandService.java b/paper/src/main/java/io/wdsj/asw/bukkit/command/AswCommandService.java index 78ff2bcb..6e5ecb3f 100644 --- a/paper/src/main/java/io/wdsj/asw/bukkit/command/AswCommandService.java +++ b/paper/src/main/java/io/wdsj/asw/bukkit/command/AswCommandService.java @@ -48,6 +48,9 @@ public void reloadAll(CommandSender sender) { plugin.reloadPluginConfiguration(); AdvancedSensitiveWords.sensitiveWordBs.destroy(); + if (AdvancedSensitiveWords.networkSensitiveWordBs != null) { + AdvancedSensitiveWords.networkSensitiveWordBs.destroy(); + } plugin.doInitTasks(); if (configuration.get(PluginSettings.BOOK_CACHE_CLEAR_ON_RELOAD) && configuration.get(PluginSettings.BOOK_CACHE)) { @@ -103,7 +106,7 @@ public void test(CommandSender sender, String text) { return; } - List censoredWords = AdvancedSensitiveWords.sensitiveWordBs.findAll(text); + List censoredWords = AdvancedSensitiveWords.findAllSensitive(text); if (censoredWords.isEmpty()) { MessageUtils.sendMessage(sender, PluginMessages.MESSAGE_ON_COMMAND_TEST_PASS); return; @@ -111,7 +114,7 @@ public void test(CommandSender sender, String text) { String message = MessageUtils.retrieveMessage(PluginMessages.MESSAGE_ON_COMMAND_TEST) .replace("%original_msg%", text) - .replace("%processed_msg%", AdvancedSensitiveWords.sensitiveWordBs.replace(text)) + .replace("%processed_msg%", AdvancedSensitiveWords.replaceSensitive(text)) .replace("%censored_list%", censoredWords.toString()); MessageUtils.sendMessage(sender, message); } @@ -229,4 +232,5 @@ private static String formatCategoryPolicies(Map ignoredCharacters; + + public CharIgnore(String ignoredCharacters) { + this(ignoredCharacters, ""); + } + + public CharIgnore(String ignoredCharacters, String protectedCharacters) { + Set ignored = new HashSet<>(StringUtil.toCharSet(ignoredCharacters == null ? "" : ignoredCharacters)); + ignored.removeAll(StringUtil.toCharSet(protectedCharacters == null ? "" : protectedCharacters)); + this.ignoredCharacters = Set.copyOf(ignored); + } -public class CharIgnore implements ISensitiveWordCharIgnore { @Override public boolean ignore(int i, String string, InnerSensitiveWordContext innerSensitiveWordContext) { - Set SET = StringUtil.toCharSet(setting(PluginSettings.IGNORE_CHAR)); char c = string.charAt(i); - return SET.contains(c); + return ignoredCharacters.contains(c); } } diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/permission/cache/CachingPermTool.java b/paper/src/main/java/io/wdsj/asw/bukkit/permission/cache/CachingPermTool.java index 18536eab..4b17694c 100644 --- a/paper/src/main/java/io/wdsj/asw/bukkit/permission/cache/CachingPermTool.java +++ b/paper/src/main/java/io/wdsj/asw/bukkit/permission/cache/CachingPermTool.java @@ -8,11 +8,15 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; +import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.event.player.PlayerKickEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -22,6 +26,8 @@ public final class CachingPermTool implements Listener { private static final Map> permissionCacheMap = new ConcurrentHashMap<>(); + private static final Map> stringPermissionCacheMap = new ConcurrentHashMap<>(); + private static final Map>> effectivePermissionCacheMap = new ConcurrentHashMap<>(); CachingPermTool(JavaPlugin plugin) { plugin.getServer().getPluginManager().registerEvents(this, plugin); @@ -37,6 +43,8 @@ public void disable() { entry.getValue().cleanUp(); } permissionCacheMap.clear(); + stringPermissionCacheMap.clear(); + effectivePermissionCacheMap.clear(); } public static boolean hasPermission(PermissionsEnum permission, @NotNull HumanEntity human) { @@ -50,13 +58,46 @@ public static boolean hasPermission(PermissionsEnum permission, @NotNull HumanEn return hasPermission; } + public static boolean hasPermission(@NotNull String permission, @NotNull HumanEntity human) { + Cache permCache = stringPermissionCacheMap.computeIfAbsent(human.getUniqueId(), + k -> CacheBuilder.newBuilder().expireAfterWrite(8, TimeUnit.SECONDS).build()); + String normalizedPermission = permission.toLowerCase(Locale.ROOT); + Boolean hasPermission = permCache.getIfPresent(normalizedPermission); + if (hasPermission == null) { + hasPermission = human.hasPermission(permission); + permCache.put(normalizedPermission, hasPermission); + } + return hasPermission; + } + + public static List effectivePermissions(@NotNull HumanEntity human) { + Cache> permCache = effectivePermissionCacheMap.computeIfAbsent(human.getUniqueId(), + k -> CacheBuilder.newBuilder().expireAfterWrite(8, TimeUnit.SECONDS).build()); + List permissions = permCache.getIfPresent("effective"); + if (permissions == null) { + List collected = new ArrayList<>(); + for (PermissionAttachmentInfo permission : human.getEffectivePermissions()) { + if (permission.getValue()) { + collected.add(permission.getPermission().toLowerCase(Locale.ROOT)); + } + } + permissions = List.copyOf(collected); + permCache.put("effective", permissions); + } + return permissions; + } + @EventHandler(priority = EventPriority.MONITOR) private void onQuit(PlayerQuitEvent event) { permissionCacheMap.remove(event.getPlayer().getUniqueId()); + stringPermissionCacheMap.remove(event.getPlayer().getUniqueId()); + effectivePermissionCacheMap.remove(event.getPlayer().getUniqueId()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) private void onKick(PlayerKickEvent event) { permissionCacheMap.remove(event.getPlayer().getUniqueId()); + stringPermissionCacheMap.remove(event.getPlayer().getUniqueId()); + effectivePermissionCacheMap.remove(event.getPlayer().getUniqueId()); } } diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/permission/option/PlayerOptionResolver.java b/paper/src/main/java/io/wdsj/asw/bukkit/permission/option/PlayerOptionResolver.java new file mode 100644 index 00000000..ee74b8ca --- /dev/null +++ b/paper/src/main/java/io/wdsj/asw/bukkit/permission/option/PlayerOptionResolver.java @@ -0,0 +1,187 @@ +package io.wdsj.asw.bukkit.permission.option; + +import io.wdsj.asw.bukkit.permission.cache.CachingPermTool; +import io.wdsj.asw.bukkit.setting.PaperConfigurationService; +import io.wdsj.asw.bukkit.type.ProcessMethod; +import org.bukkit.entity.Player; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +public final class PlayerOptionResolver { + static final String PREFIX = "advancedsensitivewords.option."; + private static final String PRIORITY_MARKER = ".priority."; + private static final int DEFAULT_PRIORITY = 0; + + private PlayerOptionResolver() { + } + + public static PlayerOptionView resolve(PaperConfigurationService configuration, Player player) { + return new PlayerOptionView(configuration, CachingPermTool.effectivePermissions(player)); + } + + static boolean resolveBoolean(String optionPath, boolean defaultValue, Collection permissions) { + Optional action = resolveAction(optionPath, permissions, ActionType.BOOLEAN); + if (action.isEmpty() || action.get().kind() == ActionKind.DEFAULT) { + return defaultValue; + } + return action.get().kind() == ActionKind.ENABLE; + } + + static ProcessMethod resolveMethod(String optionPath, ProcessMethod defaultValue, Collection permissions) { + Optional action = resolveAction(optionPath, permissions, ActionType.METHOD); + if (action.isEmpty() || action.get().kind() == ActionKind.DEFAULT) { + return defaultValue; + } + return action.get().kind() == ActionKind.CANCEL ? ProcessMethod.CANCEL : ProcessMethod.REPLACE; + } + + static int resolveInteger(String optionPath, int defaultValue, Collection permissions) { + Optional value = resolveNumber(optionPath, permissions); + return value.map(number -> (int) Math.floor(number.value())).orElse(defaultValue); + } + + static double resolveDouble(String optionPath, double defaultValue, Collection permissions) { + Optional value = resolveNumber(optionPath, permissions); + return value.map(ResolvedNumber::value).orElse(defaultValue); + } + + private static Optional resolveAction( + String optionPath, + Collection permissions, + ActionType actionType + ) { + String optionPrefix = PREFIX + optionPath.toLowerCase(Locale.ROOT) + "."; + ResolvedAction selected = null; + for (String rawPermission : permissions) { + String permission = rawPermission.toLowerCase(Locale.ROOT); + if (!permission.startsWith(optionPrefix)) { + continue; + } + ParsedSuffix parsed = parseSuffix(permission.substring(optionPrefix.length())); + ActionKind kind = switch (parsed.action()) { + case "default" -> ActionKind.DEFAULT; + case "enable" -> actionType == ActionType.BOOLEAN ? ActionKind.ENABLE : null; + case "disable" -> actionType == ActionType.BOOLEAN ? ActionKind.DISABLE : null; + case "cancel" -> actionType == ActionType.METHOD ? ActionKind.CANCEL : null; + case "replace" -> actionType == ActionType.METHOD ? ActionKind.REPLACE : null; + default -> null; + }; + if (kind == null) { + continue; + } + ResolvedAction candidate = new ResolvedAction(parsed.priority(), kind); + if (selected == null || candidate.compareTo(selected) > 0) { + selected = candidate; + } + } + return Optional.ofNullable(selected); + } + + private static Optional resolveNumber(String optionPath, Collection permissions) { + String optionPrefix = PREFIX + optionPath.toLowerCase(Locale.ROOT) + "."; + ResolvedNumber selected = null; + ResolvedDefault selectedDefault = null; + for (String rawPermission : permissions) { + String permission = rawPermission.toLowerCase(Locale.ROOT); + if (!permission.startsWith(optionPrefix)) { + continue; + } + ParsedSuffix parsed = parseSuffix(permission.substring(optionPrefix.length())); + if ("default".equals(parsed.action())) { + ResolvedDefault candidate = new ResolvedDefault(parsed.priority()); + if (selectedDefault == null || candidate.priority() > selectedDefault.priority()) { + selectedDefault = candidate; + } + continue; + } + if (!parsed.action().startsWith("value.")) { + continue; + } + String valueText = parsed.action().substring("value.".length()); + double value; + try { + value = Double.parseDouble(valueText); + } catch (NumberFormatException exception) { + continue; + } + if (!Double.isFinite(value) || value < 0.0D) { + continue; + } + ResolvedNumber candidate = new ResolvedNumber(parsed.priority(), value); + if (selected == null || candidate.compareTo(selected) > 0) { + selected = candidate; + } + } + if (selected == null) { + return Optional.empty(); + } + if (selectedDefault != null && selectedDefault.priority() > selected.priority()) { + return Optional.empty(); + } + return Optional.of(selected); + } + + private static ParsedSuffix parseSuffix(String suffix) { + int priorityIndex = suffix.lastIndexOf(PRIORITY_MARKER); + if (priorityIndex < 0) { + return new ParsedSuffix(suffix, DEFAULT_PRIORITY); + } + String action = suffix.substring(0, priorityIndex); + String priorityText = suffix.substring(priorityIndex + PRIORITY_MARKER.length()); + try { + return new ParsedSuffix(action, Integer.parseInt(priorityText)); + } catch (NumberFormatException exception) { + return new ParsedSuffix(action, DEFAULT_PRIORITY); + } + } + + private enum ActionType { + BOOLEAN, + METHOD + } + + private enum ActionKind { + DEFAULT(0), + ENABLE(1), + DISABLE(2), + REPLACE(1), + CANCEL(2); + + private final int strictness; + + ActionKind(int strictness) { + this.strictness = strictness; + } + } + + private record ParsedSuffix(String action, int priority) { + } + + private record ResolvedDefault(int priority) { + } + + private record ResolvedAction(int priority, ActionKind kind) implements Comparable { + @Override + public int compareTo(ResolvedAction other) { + int priorityCompare = Integer.compare(priority, other.priority); + if (priorityCompare != 0) { + return priorityCompare; + } + return Integer.compare(kind.strictness, other.kind.strictness); + } + } + + private record ResolvedNumber(int priority, double value) implements Comparable { + @Override + public int compareTo(ResolvedNumber other) { + int priorityCompare = Integer.compare(priority, other.priority); + if (priorityCompare != 0) { + return priorityCompare; + } + return Double.compare(value, other.value); + } + } +} diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/permission/option/PlayerOptionView.java b/paper/src/main/java/io/wdsj/asw/bukkit/permission/option/PlayerOptionView.java new file mode 100644 index 00000000..5cf3c4c0 --- /dev/null +++ b/paper/src/main/java/io/wdsj/asw/bukkit/permission/option/PlayerOptionView.java @@ -0,0 +1,34 @@ +package io.wdsj.asw.bukkit.permission.option; + +import io.wdsj.asw.bukkit.setting.PaperConfigurationService; +import io.wdsj.asw.bukkit.setting.SettingKey; +import io.wdsj.asw.bukkit.type.ProcessMethod; + +import java.util.List; +import java.util.Objects; + +public final class PlayerOptionView { + private final PaperConfigurationService configuration; + private final List permissions; + + PlayerOptionView(PaperConfigurationService configuration, List permissions) { + this.configuration = Objects.requireNonNull(configuration, "configuration"); + this.permissions = List.copyOf(permissions); + } + + public boolean bool(String optionPath, SettingKey defaultKey) { + return PlayerOptionResolver.resolveBoolean(optionPath, configuration.get(defaultKey), permissions); + } + + public ProcessMethod method(String optionPath, SettingKey defaultKey) { + return PlayerOptionResolver.resolveMethod(optionPath, configuration.get(defaultKey), permissions); + } + + public int integer(String optionPath, SettingKey defaultKey) { + return PlayerOptionResolver.resolveInteger(optionPath, configuration.get(defaultKey), permissions); + } + + public double decimal(String optionPath, SettingKey defaultKey) { + return PlayerOptionResolver.resolveDouble(optionPath, configuration.get(defaultKey), permissions); + } +} diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/permission/option/PlayerOptions.java b/paper/src/main/java/io/wdsj/asw/bukkit/permission/option/PlayerOptions.java new file mode 100644 index 00000000..7aba4dce --- /dev/null +++ b/paper/src/main/java/io/wdsj/asw/bukkit/permission/option/PlayerOptions.java @@ -0,0 +1,52 @@ +package io.wdsj.asw.bukkit.permission.option; + +/** + * Player-scoped option permission paths below {@code advancedsensitivewords.option}. + */ +public final class PlayerOptions { + public static final String CHAT_METHOD = "chat.method"; + public static final String CHAT_FAKE_MESSAGE_ON_CANCEL = "chat.fake-message-on-cancel"; + public static final String CHAT_SEND_MESSAGE = "chat.send-message"; + public static final String CHAT_CONTEXT_CHECK = "chat.context-check"; + public static final String CHAT_ANTI_SPAM_ENABLED = "chat.anti-spam.enabled"; + public static final String CHAT_ANTI_SPAM_SEND_MESSAGE = "chat.anti-spam.send-message"; + public static final String CHAT_ANTI_SPAM_RATE_LIMIT_ENABLED = "chat.anti-spam.rate-limit.enabled"; + public static final String CHAT_ANTI_SPAM_MINIMUM_ENTROPY_CODE_POINTS = "chat.anti-spam.minimum-entropy-code-points"; + public static final String CHAT_ANTI_SPAM_MINIMUM_SIMILARITY_CODE_POINTS = "chat.anti-spam.minimum-similarity-code-points"; + public static final String CHAT_ANTI_SPAM_HISTORY_SIZE = "chat.anti-spam.history-size"; + public static final String CHAT_ANTI_SPAM_HISTORY_MAX_AGE_SECONDS = "chat.anti-spam.history-max-age-seconds"; + public static final String CHAT_ANTI_SPAM_SIMILAR_CHECK_AMOUNT = "chat.anti-spam.similar-check-amount"; + public static final String CHAT_ANTI_SPAM_SIMILAR_MIN_DISTANCE = "chat.anti-spam.similar-min-distance"; + public static final String CHAT_ANTI_SPAM_SIMILAR_MAX_SIMILARITY = "chat.anti-spam.similar-max-similarity"; + public static final String CHAT_ANTI_SPAM_RATE_LIMIT_CAPACITY = "chat.anti-spam.rate-limit.capacity"; + public static final String CHAT_ANTI_SPAM_RATE_LIMIT_REFILL_INTERVAL_SECONDS = "chat.anti-spam.rate-limit.refill-interval-seconds"; + + public static final String BOOK_METHOD = "book.method"; + public static final String BOOK_IGNORE_NEW_LINE = "book.ignore-new-line"; + public static final String BOOK_CROSS_PAGE_CHECK = "book.cross-page-check"; + public static final String BOOK_SEND_MESSAGE = "book.send-message"; + public static final String BOOK_CACHE_ENABLE_CACHE = "book.cache.enable-cache"; + + public static final String SIGN_METHOD = "sign.method"; + public static final String SIGN_FAKE_ON_CANCEL = "sign.fake-on-cancel"; + public static final String SIGN_MULTI_LINE_CHECK = "sign.multi-line-check"; + public static final String SIGN_CONTEXT_CHECK = "sign.context-check"; + public static final String SIGN_CONTEXT_MAX_SIZE = "sign.context-max-size"; + public static final String SIGN_CONTEXT_MAX_TIME = "sign.context-max-time"; + public static final String SIGN_SEND_MESSAGE = "sign.send-message"; + + public static final String ANVIL_METHOD = "anvil.method"; + public static final String ANVIL_SEND_MESSAGE = "anvil.send-message"; + + public static final String ITEM_METHOD = "item.method"; + public static final String ITEM_SEND_MESSAGE = "item.send-message"; + + public static final String AI_ENABLED = "ai.enabled"; + public static final String AI_MINIMUM_MESSAGE_CODE_POINTS = "ai.minimum-message-code-points"; + public static final String AI_MAXIMUM_MESSAGE_CODE_POINTS = "ai.maximum-message-code-points"; + public static final String AI_MINIMUM_ENTROPY_BITS = "ai.minimum-entropy-bits"; + public static final String AI_PER_PLAYER_COOLDOWN_SECONDS = "ai.per-player-cooldown-seconds"; + + private PlayerOptions() { + } +} diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/setting/PaperConfigurationService.java b/paper/src/main/java/io/wdsj/asw/bukkit/setting/PaperConfigurationService.java index c2023bba..cfaa1870 100644 --- a/paper/src/main/java/io/wdsj/asw/bukkit/setting/PaperConfigurationService.java +++ b/paper/src/main/java/io/wdsj/asw/bukkit/setting/PaperConfigurationService.java @@ -249,6 +249,12 @@ private static void validateAntiSpam(SettingsConfiguration.AntiSpam antiSpam) { || antiSpam.similarMaxSimilarity > 1.1D) { throw new IllegalArgumentException("chat.anti-spam.similar-max-similarity must be between 0.0 and 1.1"); } + if (antiSpam.rateLimit == null) { + throw new IllegalArgumentException("chat.anti-spam.rate-limit cannot be null"); + } + if (antiSpam.rateLimit.capacity < 1 || antiSpam.rateLimit.refillIntervalSeconds < 1) { + throw new IllegalArgumentException("chat.anti-spam.rate-limit settings must be at least 1"); + } } private static void validateHttpUrl(String value, String settingName) { diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/setting/PluginSettings.java b/paper/src/main/java/io/wdsj/asw/bukkit/setting/PluginSettings.java index 01955a85..93caa144 100644 --- a/paper/src/main/java/io/wdsj/asw/bukkit/setting/PluginSettings.java +++ b/paper/src/main/java/io/wdsj/asw/bukkit/setting/PluginSettings.java @@ -24,7 +24,6 @@ public final class PluginSettings { public static final SettingKey NOTICE_OPERATOR = key(settings -> settings.plugin.noticeOperator); public static final SettingKey> MANUAL_PUNISHMENT = key(settings -> settings.plugin.manualPunishment); public static final SettingKey VIOLATION_RESET_TIME = key(settings -> settings.plugin.violationResetTime); - public static final SettingKey ONLY_RESET_ONLINE_PLAYERS = key(settings -> settings.plugin.onlyResetOnlinePlayers); public static final SettingKey PURGE_LOG_FILE = key(settings -> settings.plugin.purgeLogFile); public static final SettingKey REPLACEMENT = key(settings -> settings.plugin.replacement); public static final SettingKey> DEFINED_REPLACEMENT = key(settings -> settings.plugin.definedReplacement); @@ -73,6 +72,9 @@ public final class PluginSettings { public static final SettingKey CHAT_ANTI_SPAM_SIMILAR_CHECK_AMOUNT = key(settings -> settings.chat.antiSpam.similarCheckAmount); public static final SettingKey CHAT_ANTI_SPAM_SIMILAR_MIN_DISTANCE = key(settings -> settings.chat.antiSpam.similarMinDistance); public static final SettingKey CHAT_ANTI_SPAM_SIMILAR_MAX_SIMILARITY = key(settings -> settings.chat.antiSpam.similarMaxSimilarity); + public static final SettingKey CHAT_ANTI_SPAM_RATE_LIMIT_ENABLED = key(settings -> settings.chat.antiSpam.rateLimit.enabled); + public static final SettingKey CHAT_ANTI_SPAM_RATE_LIMIT_CAPACITY = key(settings -> settings.chat.antiSpam.rateLimit.capacity); + public static final SettingKey CHAT_ANTI_SPAM_RATE_LIMIT_REFILL_INTERVAL_SECONDS = key(settings -> settings.chat.antiSpam.rateLimit.refillIntervalSeconds); public static final SettingKey CHAT_ANTI_SPAM_SEND_MESSAGE = key(settings -> settings.chat.antiSpam.sendMessage); public static final SettingKey> CHAT_PUNISHMENT = key(settings -> settings.chat.punishment); public static final SettingKey CHAT_BROADCAST_CHECK = key(settings -> settings.chat.broadcastCheck); diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/setting/SettingsConfiguration.java b/paper/src/main/java/io/wdsj/asw/bukkit/setting/SettingsConfiguration.java index d0817f89..4006bc2f 100644 --- a/paper/src/main/java/io/wdsj/asw/bukkit/setting/SettingsConfiguration.java +++ b/paper/src/main/java/io/wdsj/asw/bukkit/setting/SettingsConfiguration.java @@ -64,8 +64,6 @@ public static final class Plugin { public List manualPunishment = new ArrayList<>(); @Comment("Violation counter reset interval in minutes.") public long violationResetTime = 20L; - @Comment("Whether to reset only the violation counters of online players.") - public boolean onlyResetOnlinePlayers = false; @Comment("Whether to clear old violation logs on startup.") public boolean purgeLogFile = false; @Comment("Replacement character used for blocked content.") @@ -92,10 +90,10 @@ public static final class Plugin { public boolean hookVelocity = false; @Comment("Plugin compatibility settings.") public Compatibility compatibility = new Compatibility(); - @Comment("Characters ignored by the sensitive-word matcher.") + @Comment("Characters ignored by the sensitive-word matcher. URL/IP/email syntax characters are protected automatically when those checks are enabled.") public String ignoreChar = "`-—=~~!!@#$%^&§*()_+[]{}\\|;:'\"“”,,.。、()<>??¥【】《》 "; @Comment("Whether to remove formatting before detection.") - public boolean enablePreProcess = false; + public boolean enablePreProcess = true; @Comment("Regular expression used by preprocessing.") public String preProcessRegex = "[§&][0-9A-Fa-fK-Ok-oRr]"; @Comment("Whether to ignore letter case.") @@ -121,7 +119,7 @@ public static final class Plugin { @Comment("Whether to check URLs.") public boolean enableUrlCheck = true; @Comment("Whether URLs without an HTTP(S) prefix should be checked.") - public boolean urlCheckNoPrefix = false; + public boolean urlCheckNoPrefix = true; @Comment("Whether to check sensitive English words.") public boolean enableWordCheck = true; @Comment("Whether to check IPv4 addresses.") @@ -204,10 +202,22 @@ public static final class AntiSpam { public int similarMinDistance = 2; @Comment("Similarity threshold from 0.0 to 1.0. Messages at or above this value are treated as spam. Use 1.1 to disable.") public double similarMaxSimilarity = 0.90D; + @Comment("Token-bucket message rate limit. This is part of chat anti-spam and applies to all checked chat messages.") + public AntiSpamRateLimit rateLimit = new AntiSpamRateLimit(); @Comment("Whether to send a message when anti-spam cancels chat.") public boolean sendMessage = true; } + @Configuration + public static final class AntiSpamRateLimit { + @Comment("Whether to enable token-bucket chat rate limiting.") + public boolean enabled = false; + @Comment("Maximum stored tokens, also the allowed burst size.") + public int capacity = 3; + @Comment("Seconds needed to refill the full bucket.") + public int refillIntervalSeconds = 30; + } + @Configuration public static final class Ai { @Comment("Whether to enable LLM-assisted chat moderation.") @@ -383,4 +393,5 @@ public static final class Item { @Comment("Punishment actions for item violations. Leave empty to disable automatic punishment.") public List punishment = new ArrayList<>(); } + } diff --git a/paper/src/main/java/io/wdsj/asw/bukkit/task/punish/ViolationResetTask.java b/paper/src/main/java/io/wdsj/asw/bukkit/task/punish/ViolationResetTask.java index 3b6bc8b9..c4cf4492 100644 --- a/paper/src/main/java/io/wdsj/asw/bukkit/task/punish/ViolationResetTask.java +++ b/paper/src/main/java/io/wdsj/asw/bukkit/task/punish/ViolationResetTask.java @@ -4,12 +4,7 @@ import io.wdsj.asw.bukkit.manage.notice.Notifier; import io.wdsj.asw.bukkit.manage.punish.ViolationCounter; import io.wdsj.asw.bukkit.setting.PluginMessages; -import io.wdsj.asw.bukkit.setting.PluginSettings; import io.wdsj.asw.bukkit.util.message.MessageUtils; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import static io.wdsj.asw.bukkit.AdvancedSensitiveWords.setting; /** * Asynchronous task to reset the violation count of players. @@ -17,13 +12,7 @@ public class ViolationResetTask extends UniversalRunnable { @Override public void run() { - if (setting(PluginSettings.ONLY_RESET_ONLINE_PLAYERS)) { - for (Player player : Bukkit.getOnlinePlayers()) { - ViolationCounter.INSTANCE.resetViolationCount(player); - } - } else { - ViolationCounter.INSTANCE.resetAllViolations(); - } + ViolationCounter.INSTANCE.resetAllViolations(); String message = MessageUtils.retrieveMessage(PluginMessages.MESSAGE_ON_VIOLATION_RESET); Notifier.normalNotice(message); } diff --git a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/AnvilListener.kt b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/AnvilListener.kt index ee9e47d3..cbf22b65 100644 --- a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/AnvilListener.kt +++ b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/AnvilListener.kt @@ -1,7 +1,10 @@ package io.wdsj.asw.bukkit.listener -import io.wdsj.asw.bukkit.AdvancedSensitiveWords.sensitiveWordBs +import io.wdsj.asw.bukkit.AdvancedSensitiveWords import io.wdsj.asw.bukkit.permission.PermissionsEnum +import io.wdsj.asw.bukkit.permission.option.PlayerOptionResolver +import io.wdsj.asw.bukkit.permission.option.PlayerOptionView +import io.wdsj.asw.bukkit.permission.option.PlayerOptions import io.wdsj.asw.bukkit.setting.PaperConfigurationService import io.wdsj.asw.bukkit.setting.PluginMessages import io.wdsj.asw.bukkit.setting.PluginSettings @@ -24,12 +27,14 @@ class AnvilListener(private val configuration: PaperConfigurationService) : List @EventHandler(priority = EventPriority.LOWEST) fun onAnvil(event: InventoryClickEvent) { - if (!configuration.get(PluginSettings.ENABLE_ANVIL_EDIT_CHECK)) return + val globalEnabled = configuration.get(PluginSettings.ENABLE_ANVIL_EDIT_CHECK) + if (!globalEnabled) return if (event.inventory.type != InventoryType.ANVIL) return if (event.rawSlot != 2) return val player = event.whoClicked as? Player ?: return if (processingGuard.shouldSkipBasic(player, PermissionsEnum.BYPASS_ANVIL)) return + val options = PlayerOptionResolver.resolve(configuration, player) val outputItem = event.currentItem ?: return if (!outputItem.hasItemMeta()) return @@ -40,24 +45,24 @@ class AnvilListener(private val configuration: PaperConfigurationService) : List val originalNameComponent = itemMeta.displayName() ?: return val originalItemName = preprocess(MessageUtils.plainText(originalNameComponent)) val startTime = System.currentTimeMillis() - val censoredWords = sensitiveWordBs.findAll(originalItemName) + val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalItemName) SensitiveFilterEvents.post(event.isAsynchronous, ModuleType.ANVIL, player, originalItemName, censoredWords) if (censoredWords.isEmpty()) return - if (isCancelMode()) { + if (isCancelMode(options)) { event.isCancelled = true } else { itemMeta.displayName( MessageUtils.replaceLiteral( originalNameComponent, originalItemName, - sensitiveWordBs.replace(originalItemName), + AdvancedSensitiveWords.replaceSensitive(originalItemName), ), ) outputItem.setItemMeta(itemMeta) } - if (configuration.get(PluginSettings.ANVIL_SEND_MESSAGE)) { + if (options.bool(PlayerOptions.ANVIL_SEND_MESSAGE, PluginSettings.ANVIL_SEND_MESSAGE)) { MessageUtils.sendMessage(player, PluginMessages.MESSAGE_ON_ANVIL_RENAME) } @@ -78,7 +83,7 @@ class AnvilListener(private val configuration: PaperConfigurationService) : List return text.replace(Utils.preProcessRegex.toRegex(), "") } - private fun isCancelMode(): Boolean { - return configuration.get(PluginSettings.ANVIL_METHOD).isCancel + private fun isCancelMode(options: PlayerOptionView): Boolean { + return options.method(PlayerOptions.ANVIL_METHOD, PluginSettings.ANVIL_METHOD).isCancel } } diff --git a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/BookListener.kt b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/BookListener.kt index 73dcd6fb..d2851fe9 100644 --- a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/BookListener.kt +++ b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/BookListener.kt @@ -1,7 +1,10 @@ package io.wdsj.asw.bukkit.listener -import io.wdsj.asw.bukkit.AdvancedSensitiveWords.sensitiveWordBs +import io.wdsj.asw.bukkit.AdvancedSensitiveWords import io.wdsj.asw.bukkit.permission.PermissionsEnum +import io.wdsj.asw.bukkit.permission.option.PlayerOptionResolver +import io.wdsj.asw.bukkit.permission.option.PlayerOptionView +import io.wdsj.asw.bukkit.permission.option.PlayerOptions import io.wdsj.asw.bukkit.setting.PaperConfigurationService import io.wdsj.asw.bukkit.setting.PluginMessages import io.wdsj.asw.bukkit.setting.PluginSettings @@ -25,25 +28,27 @@ class BookListener(private val configuration: PaperConfigurationService) : Liste @EventHandler(priority = EventPriority.LOWEST) fun onBook(event: PlayerEditBookEvent) { - if (!configuration.get(PluginSettings.ENABLE_BOOK_EDIT_CHECK)) return + val globalEnabled = configuration.get(PluginSettings.ENABLE_BOOK_EDIT_CHECK) + if (!globalEnabled) return val player = event.player if (processingGuard.shouldSkipBasic(player, PermissionsEnum.BYPASS_BOOK)) return + val options = PlayerOptionResolver.resolve(configuration, player) val startTime = System.currentTimeMillis() val bookMeta = event.newBookMeta - var violation = censorPages(event, bookMeta) + var violation = censorPages(event, bookMeta, options) if (violation == null) { - violation = censorCrossPage(event) + violation = censorCrossPage(event, options) } - censorAuthor(event, bookMeta)?.let { violation = it } - censorTitle(event, bookMeta)?.let { violation = it } + censorAuthor(event, bookMeta, options)?.let { violation = it } + censorTitle(event, bookMeta, options)?.let { violation = it } val finalViolation = violation ?: return event.newBookMeta = bookMeta - if (configuration.get(PluginSettings.BOOK_SEND_MESSAGE)) { + if (options.bool(PlayerOptions.BOOK_SEND_MESSAGE, PluginSettings.BOOK_SEND_MESSAGE)) { MessageUtils.sendMessage(player, PluginMessages.MESSAGE_ON_BOOK) } @@ -59,35 +64,35 @@ class BookListener(private val configuration: PaperConfigurationService) : Liste ) } - private fun censorPages(event: PlayerEditBookEvent, bookMeta: BookMeta): BookViolation? { + private fun censorPages(event: PlayerEditBookEvent, bookMeta: BookMeta, options: PlayerOptionView): BookViolation? { if (!bookMeta.hasPages()) return null var violation: BookViolation? = null for (pageIndex in 1..bookMeta.pageCount) { val page = bookMeta.page(pageIndex) - val originalPage = preprocessPage(page) - val censoredWords = findPageCensoredWords(originalPage) + val originalPage = preprocessPage(page, options) + val censoredWords = findPageCensoredWords(originalPage, options) SensitiveFilterEvents.post(event.isAsynchronous, ModuleType.BOOK, event.player, originalPage, censoredWords) if (censoredWords.isEmpty()) continue violation = BookViolation(originalPage, censoredWords) - if (isCancelMode()) { + if (isCancelMode(options)) { event.isCancelled = true break } - bookMeta.page(pageIndex, replacePage(page, originalPage, censoredWords)) + bookMeta.page(pageIndex, replacePage(page, originalPage, censoredWords, options)) } return violation } - private fun censorCrossPage(event: PlayerEditBookEvent): BookViolation? { - if (!configuration.get(PluginSettings.BOOK_CROSS_PAGE)) return null + private fun censorCrossPage(event: PlayerEditBookEvent, options: PlayerOptionView): BookViolation? { + if (!options.bool(PlayerOptions.BOOK_CROSS_PAGE_CHECK, PluginSettings.BOOK_CROSS_PAGE)) return null val originalPageCrossed = (1..event.newBookMeta.pageCount) .joinToString("") { MessageUtils.plainText(event.newBookMeta.page(it)) } - .let { preprocessPageText(it) } - val censoredWords = sensitiveWordBs.findAll(originalPageCrossed) + .let { preprocessPageText(it, options) } + val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalPageCrossed) SensitiveFilterEvents.post(event.isAsynchronous, ModuleType.BOOK, event.player, originalPageCrossed, censoredWords) if (censoredWords.isEmpty()) return null @@ -95,65 +100,65 @@ class BookListener(private val configuration: PaperConfigurationService) : Liste return BookViolation(originalPageCrossed, censoredWords) } - private fun censorAuthor(event: PlayerEditBookEvent, bookMeta: BookMeta): BookViolation? { + private fun censorAuthor(event: PlayerEditBookEvent, bookMeta: BookMeta, options: PlayerOptionView): BookViolation? { val author = event.newBookMeta.author() ?: return null val originalAuthor = preprocessText(MessageUtils.plainText(author)) - val censoredWords = sensitiveWordBs.findAll(originalAuthor) + val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalAuthor) SensitiveFilterEvents.post(event.isAsynchronous, ModuleType.BOOK, event.player, originalAuthor, censoredWords) if (censoredWords.isEmpty()) return null - if (isCancelMode()) { + if (isCancelMode(options)) { event.isCancelled = true } else { - bookMeta.author(MessageUtils.replaceLiteral(author, originalAuthor, sensitiveWordBs.replace(originalAuthor))) + bookMeta.author(MessageUtils.replaceLiteral(author, originalAuthor, AdvancedSensitiveWords.replaceSensitive(originalAuthor))) } return BookViolation(originalAuthor, censoredWords) } - private fun censorTitle(event: PlayerEditBookEvent, bookMeta: BookMeta): BookViolation? { + private fun censorTitle(event: PlayerEditBookEvent, bookMeta: BookMeta, options: PlayerOptionView): BookViolation? { val title = event.newBookMeta.title() ?: return null val originalTitle = preprocessText(MessageUtils.plainText(title)) - val censoredWords = sensitiveWordBs.findAll(originalTitle) + val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalTitle) SensitiveFilterEvents.post(event.isAsynchronous, ModuleType.BOOK, event.player, originalTitle, censoredWords) if (censoredWords.isEmpty()) return null - if (isCancelMode()) { + if (isCancelMode(options)) { event.isCancelled = true } else { - bookMeta.title(MessageUtils.replaceLiteral(title, originalTitle, sensitiveWordBs.replace(originalTitle))) + bookMeta.title(MessageUtils.replaceLiteral(title, originalTitle, AdvancedSensitiveWords.replaceSensitive(originalTitle))) } return BookViolation(originalTitle, censoredWords) } - private fun findPageCensoredWords(page: String): List { - if (!configuration.get(PluginSettings.BOOK_CACHE)) { - return sensitiveWordBs.findAll(page) + private fun findPageCensoredWords(page: String, options: PlayerOptionView): List { + if (!options.bool(PlayerOptions.BOOK_CACHE_ENABLE_CACHE, PluginSettings.BOOK_CACHE)) { + return AdvancedSensitiveWords.findAllSensitive(page) } if (BookCache.isBookCached(page)) { return BookCache.getCachedBookSensitiveWordList(page) } - return sensitiveWordBs.findAll(page) + return AdvancedSensitiveWords.findAllSensitive(page) } - private fun replacePage(page: Component, pagePlainText: String, censoredWords: List): Component { - if (!configuration.get(PluginSettings.BOOK_CACHE)) { - return MessageUtils.replaceLiteral(page, pagePlainText, sensitiveWordBs.replace(pagePlainText)) + private fun replacePage(page: Component, pagePlainText: String, censoredWords: List, options: PlayerOptionView): Component { + if (!options.bool(PlayerOptions.BOOK_CACHE_ENABLE_CACHE, PluginSettings.BOOK_CACHE)) { + return MessageUtils.replaceLiteral(page, pagePlainText, AdvancedSensitiveWords.replaceSensitive(pagePlainText)) } if (BookCache.isBookCached(pagePlainText)) { return MessageUtils.replaceLiteral(page, pagePlainText, BookCache.getCachedProcessedBookContent(pagePlainText)) } - val processedPage = sensitiveWordBs.replace(pagePlainText) + val processedPage = AdvancedSensitiveWords.replaceSensitive(pagePlainText) BookCache.addToBookCache(pagePlainText, processedPage, censoredWords) return MessageUtils.replaceLiteral(page, pagePlainText, processedPage) } - private fun preprocessPage(page: Component): String { - return preprocessPageText(MessageUtils.plainText(page)) + private fun preprocessPage(page: Component, options: PlayerOptionView): String { + return preprocessPageText(MessageUtils.plainText(page), options) } - private fun preprocessPageText(text: String): String { - val lineNormalized = if (configuration.get(PluginSettings.BOOK_IGNORE_NEWLINE)) { + private fun preprocessPageText(text: String, options: PlayerOptionView): String { + val lineNormalized = if (options.bool(PlayerOptions.BOOK_IGNORE_NEW_LINE, PluginSettings.BOOK_IGNORE_NEWLINE)) { text.replace("\n", "").replace("§0", "") } else { text @@ -166,8 +171,8 @@ class BookListener(private val configuration: PaperConfigurationService) : Liste return text.replace(Utils.preProcessRegex.toRegex(), "") } - private fun isCancelMode(): Boolean { - return configuration.get(PluginSettings.BOOK_METHOD).isCancel + private fun isCancelMode(options: PlayerOptionView): Boolean { + return options.method(PlayerOptions.BOOK_METHOD, PluginSettings.BOOK_METHOD).isCancel } private data class BookViolation( diff --git a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/BroadcastListener.kt b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/BroadcastListener.kt index 73476c06..87f628ac 100644 --- a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/BroadcastListener.kt +++ b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/BroadcastListener.kt @@ -1,7 +1,7 @@ package io.wdsj.asw.bukkit.listener import io.wdsj.asw.bukkit.AdvancedSensitiveWords.isInitialized -import io.wdsj.asw.bukkit.AdvancedSensitiveWords.sensitiveWordBs +import io.wdsj.asw.bukkit.AdvancedSensitiveWords import io.wdsj.asw.bukkit.setting.PaperConfigurationService import io.wdsj.asw.bukkit.setting.PluginSettings import io.wdsj.asw.bukkit.type.ModuleType @@ -24,7 +24,7 @@ class BroadcastListener(private val configuration: PaperConfigurationService) : val originalComponent = event.message() val originalMessage = preprocess(MessageUtils.plainText(originalComponent)) val startTime = System.currentTimeMillis() - val censoredWords = sensitiveWordBs.findAll(originalMessage) + val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalMessage) SensitiveFilterEvents.post( event.isAsynchronous, ModuleType.BROADCAST, @@ -43,7 +43,7 @@ class BroadcastListener(private val configuration: PaperConfigurationService) : MessageUtils.replaceLiteral( originalComponent, originalMessage, - sensitiveWordBs.replace(originalMessage), + AdvancedSensitiveWords.replaceSensitive(originalMessage), ), ) } diff --git a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/CommandListener.kt b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/CommandListener.kt index 3cd9c0f5..7a9a1bf3 100644 --- a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/CommandListener.kt +++ b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/CommandListener.kt @@ -1,8 +1,11 @@ package io.wdsj.asw.bukkit.listener -import io.wdsj.asw.bukkit.AdvancedSensitiveWords.sensitiveWordBs +import io.wdsj.asw.bukkit.AdvancedSensitiveWords import io.wdsj.asw.bukkit.listener.command.CommandArgumentRuleSet import io.wdsj.asw.bukkit.permission.PermissionsEnum +import io.wdsj.asw.bukkit.permission.option.PlayerOptionResolver +import io.wdsj.asw.bukkit.permission.option.PlayerOptionView +import io.wdsj.asw.bukkit.permission.option.PlayerOptions import io.wdsj.asw.bukkit.setting.PaperConfigurationService import io.wdsj.asw.bukkit.setting.PluginMessages import io.wdsj.asw.bukkit.setting.PluginSettings @@ -24,22 +27,24 @@ class CommandListener(private val configuration: PaperConfigurationService) : Li @EventHandler(priority = EventPriority.LOWEST) fun onCommand(event: PlayerCommandPreprocessEvent) { - if (!configuration.get(PluginSettings.ENABLE_CHAT_CHECK)) return + val globalEnabled = configuration.get(PluginSettings.ENABLE_CHAT_CHECK) + if (!globalEnabled) return val originalCommand = preprocess(event.message) val player = event.player if (processingGuard.shouldSkip(player, PermissionsEnum.BYPASS_COMMAND)) return + val options = PlayerOptionResolver.resolve(configuration, player) val selection = configuration.commandArgumentRules().select(originalCommand) if (!configuration.shouldInspectCommand(selection) || selection.segments().isEmpty()) return val startTime = System.currentTimeMillis() - val censoredWords = selection.segments().flatMap { segment -> sensitiveWordBs.findAll(segment.content()) } + val censoredWords = selection.segments().flatMap { segment -> AdvancedSensitiveWords.findAllSensitive(segment.content()) } SensitiveFilterEvents.post(event.isAsynchronous, ModuleType.CHAT, player, selection.scannedContent(), censoredWords) if (censoredWords.isEmpty()) return - applyCommandAction(event, selection) - recordViolation(event, player, selection.scannedContent(), censoredWords, startTime) + applyCommandAction(event, selection, options) + recordViolation(event, player, options, selection.scannedContent(), censoredWords, startTime) } private fun preprocess(message: String): String { @@ -50,24 +55,26 @@ class CommandListener(private val configuration: PaperConfigurationService) : Li private fun applyCommandAction( event: PlayerCommandPreprocessEvent, selection: CommandArgumentRuleSet.CommandSelection, + options: PlayerOptionView, ) { - if (isCancelMode()) { + if (isCancelMode(options)) { event.isCancelled = true return } - val processedCommand = selection.replaceSelected(sensitiveWordBs::replace) + val processedCommand = selection.replaceSelected(AdvancedSensitiveWords::replaceSensitive) event.message = if (Utils.isCommand(processedCommand)) processedCommand else "/$processedCommand" } private fun recordViolation( event: PlayerCommandPreprocessEvent, player: Player, + options: PlayerOptionView, originalCommand: String, censoredWords: List, startTime: Long, ) { - if (configuration.get(PluginSettings.CHAT_SEND_MESSAGE)) { + if (options.bool(PlayerOptions.CHAT_SEND_MESSAGE, PluginSettings.CHAT_SEND_MESSAGE)) { MessageUtils.sendMessage( player, configuration.message(PluginMessages.MESSAGE_ON_CHAT) @@ -88,7 +95,7 @@ class CommandListener(private val configuration: PaperConfigurationService) : Li ) } - private fun isCancelMode(): Boolean { - return configuration.get(PluginSettings.CHAT_METHOD).isCancel + private fun isCancelMode(options: PlayerOptionView): Boolean { + return options.method(PlayerOptions.CHAT_METHOD, PluginSettings.CHAT_METHOD).isCancel } } diff --git a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/PlayerItemListener.kt b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/PlayerItemListener.kt index 30352265..b89f1932 100644 --- a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/PlayerItemListener.kt +++ b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/PlayerItemListener.kt @@ -1,7 +1,10 @@ package io.wdsj.asw.bukkit.listener -import io.wdsj.asw.bukkit.AdvancedSensitiveWords.sensitiveWordBs +import io.wdsj.asw.bukkit.AdvancedSensitiveWords import io.wdsj.asw.bukkit.permission.PermissionsEnum +import io.wdsj.asw.bukkit.permission.option.PlayerOptionResolver +import io.wdsj.asw.bukkit.permission.option.PlayerOptionView +import io.wdsj.asw.bukkit.permission.option.PlayerOptions import io.wdsj.asw.bukkit.setting.PaperConfigurationService import io.wdsj.asw.bukkit.setting.PluginMessages import io.wdsj.asw.bukkit.setting.PluginSettings @@ -29,38 +32,44 @@ class PlayerItemListener(private val configuration: PaperConfigurationService) : @EventHandler(priority = EventPriority.LOW) fun onPlayerHeldItem(event: PlayerItemHeldEvent) { - if (!configuration.get(PluginSettings.ENABLE_PLAYER_ITEM_CHECK)) return + val globalEnabled = configuration.get(PluginSettings.ENABLE_PLAYER_ITEM_CHECK) + if (!globalEnabled) return val player = event.player if (processingGuard.shouldSkipBasic(player, PermissionsEnum.BYPASS_ITEM)) return + val options = PlayerOptionResolver.resolve(configuration, player) val item = player.inventory.getItem(event.newSlot) ?: return - censorItemName(player, item, event) + censorItemName(player, item, event, options) } @EventHandler(priority = EventPriority.LOW) fun onDrop(event: PlayerDropItemEvent) { - if (!configuration.get(PluginSettings.ENABLE_PLAYER_ITEM_CHECK)) return + val globalEnabled = configuration.get(PluginSettings.ENABLE_PLAYER_ITEM_CHECK) + if (!globalEnabled) return val player = event.player if (processingGuard.shouldSkipBasic(player, PermissionsEnum.BYPASS_ITEM)) return + val options = PlayerOptionResolver.resolve(configuration, player) - censorItemName(player, event.itemDrop.itemStack, event) + censorItemName(player, event.itemDrop.itemStack, event, options) } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) fun onClick(event: InventoryClickEvent) { - if (!configuration.get(PluginSettings.ENABLE_PLAYER_ITEM_CHECK)) return + val globalEnabled = configuration.get(PluginSettings.ENABLE_PLAYER_ITEM_CHECK) + if (!globalEnabled) return val player = event.whoClicked as? Player ?: return if (processingGuard.shouldSkipBasic(player, PermissionsEnum.BYPASS_ITEM)) return if (event.clickedInventory?.type != InventoryType.PLAYER) return + val options = PlayerOptionResolver.resolve(configuration, player) val item = event.currentItem ?: return - censorItemName(player, item, event) + censorItemName(player, item, event, options) } - private fun censorItemName(player: Player, item: ItemStack, event: Cancellable) { + private fun censorItemName(player: Player, item: ItemStack, event: Cancellable, options: PlayerOptionView) { if (!item.hasItemMeta()) return val meta = item.itemMeta ?: return @@ -70,24 +79,24 @@ class PlayerItemListener(private val configuration: PaperConfigurationService) : val originalName = preprocess(MessageUtils.plainText(originalNameComponent)) val startTime = System.currentTimeMillis() val asynchronous = (event as? Event)?.isAsynchronous ?: false - val censoredWords = sensitiveWordBs.findAll(originalName) + val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalName) SensitiveFilterEvents.post(asynchronous, ModuleType.ITEM, player, originalName, censoredWords) if (censoredWords.isEmpty()) return - if (isCancelMode()) { + if (isCancelMode(options)) { event.isCancelled = true } else { meta.displayName( MessageUtils.replaceLiteral( originalNameComponent, originalName, - sensitiveWordBs.replace(originalName), + AdvancedSensitiveWords.replaceSensitive(originalName), ), ) item.setItemMeta(meta) } - if (configuration.get(PluginSettings.ITEM_SEND_MESSAGE)) { + if (options.bool(PlayerOptions.ITEM_SEND_MESSAGE, PluginSettings.ITEM_SEND_MESSAGE)) { MessageUtils.sendMessage(player, PluginMessages.MESSAGE_ON_ITEM) } @@ -107,7 +116,7 @@ class PlayerItemListener(private val configuration: PaperConfigurationService) : return text.replace(Utils.preProcessRegex.toRegex(), "") } - private fun isCancelMode(): Boolean { - return configuration.get(PluginSettings.ITEM_METHOD).isCancel + private fun isCancelMode(options: PlayerOptionView): Boolean { + return options.method(PlayerOptions.ITEM_METHOD, PluginSettings.ITEM_METHOD).isCancel } } diff --git a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/PlayerLoginListener.kt b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/PlayerLoginListener.kt index 5d69e91c..b39156c4 100644 --- a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/PlayerLoginListener.kt +++ b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/PlayerLoginListener.kt @@ -1,7 +1,7 @@ package io.wdsj.asw.bukkit.listener import io.papermc.paper.event.player.PlayerServerFullCheckEvent -import io.wdsj.asw.bukkit.AdvancedSensitiveWords.sensitiveWordBs +import io.wdsj.asw.bukkit.AdvancedSensitiveWords import io.wdsj.asw.bukkit.setting.PaperConfigurationService import io.wdsj.asw.bukkit.manage.notice.Notifier import io.wdsj.asw.bukkit.setting.PluginMessages @@ -26,7 +26,7 @@ class PlayerLoginListener(private val configuration: PaperConfigurationService) val playerName = event.playerProfile.name ?: return val startTime = System.currentTimeMillis() - val censoredWords = sensitiveWordBs.findAll(playerName) + val censoredWords = AdvancedSensitiveWords.findAllSensitive(playerName) SensitiveFilterEvents.post( event.isAsynchronous, ModuleType.NAME, diff --git a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/SignListener.kt b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/SignListener.kt index 50aa697b..01e6dedc 100644 --- a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/SignListener.kt +++ b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/SignListener.kt @@ -1,10 +1,11 @@ package io.wdsj.asw.bukkit.listener import com.github.houbb.sensitive.word.api.IWordResult -import com.github.houbb.sensitive.word.support.result.WordResultHandlers import io.wdsj.asw.bukkit.AdvancedSensitiveWords -import io.wdsj.asw.bukkit.AdvancedSensitiveWords.sensitiveWordBs import io.wdsj.asw.bukkit.permission.PermissionsEnum +import io.wdsj.asw.bukkit.permission.option.PlayerOptionResolver +import io.wdsj.asw.bukkit.permission.option.PlayerOptionView +import io.wdsj.asw.bukkit.permission.option.PlayerOptions import io.wdsj.asw.bukkit.setting.PaperConfigurationService import io.wdsj.asw.bukkit.integration.packetevents.sign.SignFakeViewService import io.wdsj.asw.bukkit.setting.PluginMessages @@ -37,21 +38,25 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) fun onSign(event: SignChangeEvent) { - if (!configuration.get(PluginSettings.ENABLE_SIGN_EDIT_CHECK)) return + val globalEnabled = configuration.get(PluginSettings.ENABLE_SIGN_EDIT_CHECK) + if (!globalEnabled) return if (event.lines().isEmpty()) return val player = event.player if (processingGuard.shouldSkipBasic(player, PermissionsEnum.BYPASS_SIGN)) return + val options = PlayerOptionResolver.resolve(configuration, player) val startTime = System.currentTimeMillis() val attemptedLines = event.lines().toList() - val lineScan = censorSingleLines(event, player) + val lineScan = censorSingleLines(event, player, options) val violation = lineScan.violation - ?: censorMultiLine(event, lineScan) - ?: censorContext(event, player) + ?: censorMultiLine(event, lineScan, options) + ?: censorContext(event, player, options) ?: return - if (isCancelMode() && !violation.context && configuration.get(PluginSettings.SIGN_FAKE_ON_CANCEL)) { + if (isCancelMode(options) && !violation.context && + options.bool(PlayerOptions.SIGN_FAKE_ON_CANCEL, PluginSettings.SIGN_FAKE_ON_CANCEL) + ) { SignFakeViewService.recordCancelledEdit( event, player, @@ -61,7 +66,7 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste ) } - if (configuration.get(PluginSettings.SIGN_SEND_MESSAGE)) { + if (options.bool(PlayerOptions.SIGN_SEND_MESSAGE, PluginSettings.SIGN_SEND_MESSAGE)) { MessageUtils.sendMessage(player, PluginMessages.MESSAGE_ON_SIGN) } @@ -97,7 +102,7 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste )) } - private fun censorSingleLines(event: SignChangeEvent, player: Player): SignLineScan { + private fun censorSingleLines(event: SignChangeEvent, player: Player, options: PlayerOptionView): SignLineScan { var violation: SignViolation? = null val cleanLineIndexes = mutableListOf() val cleanLineContent = StringBuilder() @@ -105,7 +110,7 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste for (lineIndex in event.lines().indices) { val originalComponent = event.line(lineIndex) ?: continue val originalMessage = preprocess(MessageUtils.plainText(originalComponent)) - val censoredWords = sensitiveWordBs.findAll(originalMessage) + val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalMessage) SensitiveFilterEvents.post(event.isAsynchronous, ModuleType.SIGN, player, originalMessage, censoredWords) if (censoredWords.isEmpty()) { @@ -117,30 +122,30 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste } violation = SignViolation(originalMessage, censoredWords) - if (isCancelMode()) { + if (isCancelMode(options)) { event.isCancelled = true continue } - val processedMessage = sensitiveWordBs.replace(originalMessage) + val processedMessage = AdvancedSensitiveWords.replaceSensitive(originalMessage) event.line(lineIndex, MessageUtils.replaceLiteral(originalComponent, originalMessage, processedMessage)) } return SignLineScan(violation, cleanLineIndexes, cleanLineContent.toString()) } - private fun censorMultiLine(event: SignChangeEvent, lineScan: SignLineScan): SignViolation? { - if (!configuration.get(PluginSettings.SIGN_MULTI_LINE_CHECK)) return null + private fun censorMultiLine(event: SignChangeEvent, lineScan: SignLineScan, options: PlayerOptionView): SignViolation? { + if (!options.bool(PlayerOptions.SIGN_MULTI_LINE_CHECK, PluginSettings.SIGN_MULTI_LINE_CHECK)) return null if (lineScan.cleanLineIndexes.isEmpty()) return null val originalContent = lineScan.cleanLineContent - val censoredWords = sensitiveWordBs.findAll(originalContent) + val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalContent) SensitiveFilterEvents.post(event.isAsynchronous, ModuleType.SIGN, event.player, originalContent, censoredWords) if (censoredWords.isEmpty()) return null - if (isCancelMode()) { + if (isCancelMode(options)) { event.isCancelled = true } else { - val processedMessage = sensitiveWordBs.replace(originalContent) + val processedMessage = AdvancedSensitiveWords.replaceSensitive(originalContent) for (lineIndex in lineScan.cleanLineIndexes) { event.line(lineIndex, MessageUtils.plainTextComponent(processedMessage)) } @@ -149,19 +154,21 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste return SignViolation(originalContent, censoredWords) } - private fun censorContext(event: SignChangeEvent, player: Player): SignViolation? { - if (!configuration.get(PluginSettings.SIGN_CONTEXT_CHECK)) return null + private fun censorContext(event: SignChangeEvent, player: Player, options: PlayerOptionView): SignViolation? { + if (!options.bool(PlayerOptions.SIGN_CONTEXT_CHECK, PluginSettings.SIGN_CONTEXT_CHECK)) return null val entry = contextEntry(event) - SignContext.addMessage(player, entry) - val entries = SignContext.getHistory(player) + val contextMaxSize = options.integer(PlayerOptions.SIGN_CONTEXT_MAX_SIZE, PluginSettings.SIGN_CONTEXT_MAX_SIZE) + val contextMaxTime = options.integer(PlayerOptions.SIGN_CONTEXT_MAX_TIME, PluginSettings.SIGN_CONTEXT_TIME_LIMIT) + SignContext.addMessage(player, entry, contextMaxSize) + val entries = SignContext.getHistory(player, contextMaxSize, contextMaxTime) val originalContext = entries.joinToString("") { it.content } - val censoredWords = sensitiveWordBs.findAll(originalContext) + val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalContext) SensitiveFilterEvents.post(event.isAsynchronous, ModuleType.SIGN, player, originalContext, censoredWords) if (censoredWords.isEmpty()) return null val resolution = resolveContext(entries, originalContext) - applyContextAction(event, resolution) + applyContextAction(event, resolution, options) SignContext.clearPlayerContext(player) return SignViolation(originalContext, censoredWords, true) } @@ -181,8 +188,8 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste ) } - private fun applyContextAction(event: SignChangeEvent, resolution: ContextResolution) { - if (isCancelMode()) { + private fun applyContextAction(event: SignChangeEvent, resolution: ContextResolution, options: PlayerOptionView) { + if (isCancelMode(options)) { event.isCancelled = true resolution.affectedEntries.forEach { scheduleSignMutation(it, null) } return @@ -236,7 +243,7 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste val replacements = Array(entries.size) { StringBuilder() } val affectedEntries = linkedSetOf() - val results = sensitiveWordBs.findAll(context, WordResultHandlers.raw()) + val results = AdvancedSensitiveWords.findAllSensitiveRaw(context) .sortedWith(compareBy { it.startIndex() }.thenByDescending { it.endIndex() }) var cursor = 0 for (result in results) { @@ -330,8 +337,8 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste return text.replace(Utils.preProcessRegex.toRegex(), "") } - private fun isCancelMode(): Boolean { - return configuration.get(PluginSettings.SIGN_METHOD).isCancel + private fun isCancelMode(options: PlayerOptionView): Boolean { + return options.method(PlayerOptions.SIGN_METHOD, PluginSettings.SIGN_METHOD).isCancel } private data class SignLineScan( diff --git a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/paper/PaperChatListener.kt b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/paper/PaperChatListener.kt index 40510307..400470aa 100644 --- a/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/paper/PaperChatListener.kt +++ b/paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/paper/PaperChatListener.kt @@ -1,11 +1,14 @@ package io.wdsj.asw.bukkit.listener.paper import io.papermc.paper.event.player.AsyncChatEvent -import io.wdsj.asw.bukkit.AdvancedSensitiveWords.sensitiveWordBs +import io.wdsj.asw.bukkit.AdvancedSensitiveWords import io.wdsj.asw.bukkit.ai.LlmChatDetectionService import io.wdsj.asw.bukkit.integration.trchat.TrChatCompat import io.wdsj.asw.bukkit.listener.abstraction.AbstractFakeMessageExecutor import io.wdsj.asw.bukkit.permission.PermissionsEnum +import io.wdsj.asw.bukkit.permission.option.PlayerOptionResolver +import io.wdsj.asw.bukkit.permission.option.PlayerOptionView +import io.wdsj.asw.bukkit.permission.option.PlayerOptions import io.wdsj.asw.bukkit.service.chat.antispam.ChatAntiSpamService import io.wdsj.asw.bukkit.setting.PaperConfigurationService import io.wdsj.asw.bukkit.setting.PluginMessages @@ -36,24 +39,26 @@ class PaperChatListener( @EventHandler(priority = EventPriority.LOWEST) fun onChat(event: AsyncChatEvent) { - if (!configuration.get(PluginSettings.ENABLE_CHAT_CHECK)) return + val globalEnabled = configuration.get(PluginSettings.ENABLE_CHAT_CHECK) + if (!globalEnabled) return val player = event.player if (processingGuard.shouldSkip(player, PermissionsEnum.BYPASS_CHAT)) return + val options = PlayerOptionResolver.resolve(configuration, player) val startTime = System.currentTimeMillis() val originalMessage = preprocess(event.message()) val originalPlainText = PlainTextComponentSerializer.plainText().serialize(originalMessage) - if (antiSpamService.check(player.uniqueId, originalPlainText) == ChatAntiSpamService.Result.SPAM) { + if (antiSpamService.check(player.uniqueId, originalPlainText, options) == ChatAntiSpamService.Result.SPAM) { event.isCancelled = true - if (configuration.get(PluginSettings.CHAT_ANTI_SPAM_SEND_MESSAGE)) { + if (options.bool(PlayerOptions.CHAT_ANTI_SPAM_SEND_MESSAGE, PluginSettings.CHAT_ANTI_SPAM_SEND_MESSAGE)) { MessageUtils.sendMessage(player, configuration.message(PluginMessages.MESSAGE_ON_CHAT_ANTI_SPAM)) } return } - val censoredWords = sensitiveWordBs.findAll(originalPlainText) + val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalPlainText) SensitiveFilterEvents.post( event.isAsynchronous, ModuleType.CHAT, @@ -63,12 +68,12 @@ class PaperChatListener( ) if (censoredWords.isNotEmpty()) { - handleDirectMessage(event, player, originalMessage, originalPlainText, censoredWords, startTime) + handleDirectMessage(event, player, options, originalMessage, originalPlainText = originalPlainText, censoredWords, startTime) return } - if (!handleContextMessage(event, player, originalPlainText, startTime)) { - llmChatDetectionService.submit(player.uniqueId, player.name, originalPlainText) + if (!handleContextMessage(event, player, options, originalPlainText, startTime)) { + llmChatDetectionService.submit(player, originalPlainText, options) } } @@ -85,22 +90,24 @@ class PaperChatListener( private fun handleDirectMessage( event: AsyncChatEvent, player: Player, + options: PlayerOptionView, originalMessage: Component, originalPlainText: String, censoredWords: List, startTime: Long, ) { - applyDirectMessageAction(event, originalMessage, originalPlainText) - recordViolation(event, player, originalPlainText, originalPlainText, censoredWords, false, startTime) + applyDirectMessageAction(event, options, originalMessage, originalPlainText) + recordViolation(event, player, options, originalPlainText, originalPlainText, censoredWords, false, startTime) } private fun applyDirectMessageAction( event: AsyncChatEvent, + options: PlayerOptionView, originalMessage: Component, originalPlainText: String, ) { - if (isCancelMode()) { - if (configuration.get(PluginSettings.CHAT_FAKE_MESSAGE_ON_CANCEL)) { + if (isCancelMode(options)) { + if (options.bool(PlayerOptions.CHAT_FAKE_MESSAGE_ON_CANCEL, PluginSettings.CHAT_FAKE_MESSAGE_ON_CANCEL)) { markFakeMessage(event.player) } else { event.isCancelled = true @@ -108,7 +115,7 @@ class PaperChatListener( return } - val processedMessage = sensitiveWordBs.replace(originalPlainText) + val processedMessage = AdvancedSensitiveWords.replaceSensitive(originalPlainText) val replacementConfig = TextReplacementConfig.builder() .matchLiteral(originalPlainText) .replacement(processedMessage) @@ -119,38 +126,40 @@ class PaperChatListener( private fun handleContextMessage( event: AsyncChatEvent, player: Player, + options: PlayerOptionView, originalPlainText: String, startTime: Long, ): Boolean { - if (!configuration.get(PluginSettings.CHAT_CONTEXT_CHECK)) return false + if (!options.bool(PlayerOptions.CHAT_CONTEXT_CHECK, PluginSettings.CHAT_CONTEXT_CHECK)) return false ChatContext.addMessage(player, originalPlainText) val originalContext = ChatContext.getHistory(player).joinToString("") - val censoredWords = sensitiveWordBs.findAll(originalContext) + val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalContext) SensitiveFilterEvents.post(event.isAsynchronous, ModuleType.CHAT, player, originalContext, censoredWords) if (censoredWords.isEmpty()) return false ChatContext.pollPlayerContext(player) - if (configuration.get(PluginSettings.CHAT_FAKE_MESSAGE_ON_CANCEL)) { + if (options.bool(PlayerOptions.CHAT_FAKE_MESSAGE_ON_CANCEL, PluginSettings.CHAT_FAKE_MESSAGE_ON_CANCEL)) { markFakeMessage(player) } else { event.isCancelled = true } - recordViolation(event, player, originalPlainText, originalContext, censoredWords, true, startTime) + recordViolation(event, player, options, originalPlainText, originalContext, censoredWords, true, startTime) return true } private fun recordViolation( event: AsyncChatEvent, player: Player, + options: PlayerOptionView, playerMessage: String, violationContent: String, censoredWords: List, contextCheck: Boolean, startTime: Long, ) { - if (configuration.get(PluginSettings.CHAT_SEND_MESSAGE)) { + if (options.bool(PlayerOptions.CHAT_SEND_MESSAGE, PluginSettings.CHAT_SEND_MESSAGE)) { MessageUtils.sendMessage( player, configuration.message(PluginMessages.MESSAGE_ON_CHAT) @@ -172,8 +181,8 @@ class PaperChatListener( ) } - private fun isCancelMode(): Boolean { - return configuration.get(PluginSettings.CHAT_METHOD).isCancel + private fun isCancelMode(options: PlayerOptionView): Boolean { + return options.method(PlayerOptions.CHAT_METHOD, PluginSettings.CHAT_METHOD).isCancel } private fun markFakeMessage(player: Player) { diff --git a/paper/src/main/kotlin/io/wdsj/asw/bukkit/service/chat/antispam/ChatAntiSpamService.kt b/paper/src/main/kotlin/io/wdsj/asw/bukkit/service/chat/antispam/ChatAntiSpamService.kt index 9f10d979..d6db8182 100644 --- a/paper/src/main/kotlin/io/wdsj/asw/bukkit/service/chat/antispam/ChatAntiSpamService.kt +++ b/paper/src/main/kotlin/io/wdsj/asw/bukkit/service/chat/antispam/ChatAntiSpamService.kt @@ -1,5 +1,7 @@ package io.wdsj.asw.bukkit.service.chat.antispam +import io.wdsj.asw.bukkit.permission.option.PlayerOptionView +import io.wdsj.asw.bukkit.permission.option.PlayerOptions import io.wdsj.asw.bukkit.setting.PaperConfigurationService import io.wdsj.asw.bukkit.setting.PluginSettings import io.wdsj.asw.bukkit.util.list.EvictingRingList @@ -17,47 +19,98 @@ class ChatAntiSpamService(private val configuration: PaperConfigurationService) .takeIf(String::isNotBlank) ?.let(Pattern::compile) - fun check(playerId: UUID, message: String, nowMillis: Long = System.currentTimeMillis()): Result { - if (!configuration.get(PluginSettings.CHAT_ANTI_SPAM_ENABLED)) return Result.CLEAN + fun check( + playerId: UUID, + message: String, + options: PlayerOptionView, + nowMillis: Long = System.currentTimeMillis(), + ): Result { + if (!options.bool(PlayerOptions.CHAT_ANTI_SPAM_ENABLED, PluginSettings.CHAT_ANTI_SPAM_ENABLED)) return Result.CLEAN + if (!consumeToken(playerId, options, nowMillis)) return Result.SPAM val preprocessed = preprocess(message) val visibleCodePoints = MessageEntropy.visibleCodePointCount(preprocessed) val normalized = normalize(preprocessed) - if (visibleCodePoints >= configuration.get(PluginSettings.CHAT_ANTI_SPAM_MINIMUM_ENTROPY_CODE_POINTS)) { + if (visibleCodePoints >= options.integer( + PlayerOptions.CHAT_ANTI_SPAM_MINIMUM_ENTROPY_CODE_POINTS, + PluginSettings.CHAT_ANTI_SPAM_MINIMUM_ENTROPY_CODE_POINTS, + ).coerceAtLeast(1) + ) { val entropy = MessageEntropy.shannonEntropyBits(normalized) val minimumEntropy = configuration.get(PluginSettings.CHAT_ANTI_SPAM_MINIMUM_ENTROPY_BITS) if (minimumEntropy >= 0.0 && entropy < minimumEntropy) { - remember(playerId, normalized, nowMillis) + remember(playerId, normalized, options, nowMillis) return Result.SPAM } val minimumAverageEntropy = configuration.get(PluginSettings.CHAT_ANTI_SPAM_MINIMUM_AVERAGE_ENTROPY) if (minimumAverageEntropy >= 0.0 && averageEntropy(visibleCodePoints, entropy) < minimumAverageEntropy) { - remember(playerId, normalized, nowMillis) + remember(playerId, normalized, options, nowMillis) return Result.SPAM } } - val result = if (visibleCodePoints >= configuration.get(PluginSettings.CHAT_ANTI_SPAM_MINIMUM_SIMILARITY_CODE_POINTS)) { - checkSimilarity(playerId, normalized, nowMillis) + val result = if (visibleCodePoints >= options.integer( + PlayerOptions.CHAT_ANTI_SPAM_MINIMUM_SIMILARITY_CODE_POINTS, + PluginSettings.CHAT_ANTI_SPAM_MINIMUM_SIMILARITY_CODE_POINTS, + ).coerceAtLeast(1) + ) { + checkSimilarity(playerId, normalized, options, nowMillis) } else { Result.CLEAN } - remember(playerId, normalized, nowMillis) + remember(playerId, normalized, options, nowMillis) return result } - private fun checkSimilarity(playerId: UUID, normalized: String, nowMillis: Long): Result { + private fun consumeToken(playerId: UUID, options: PlayerOptionView, nowMillis: Long): Boolean { + if (!options.bool( + PlayerOptions.CHAT_ANTI_SPAM_RATE_LIMIT_ENABLED, + PluginSettings.CHAT_ANTI_SPAM_RATE_LIMIT_ENABLED, + ) + ) return true + + val capacity = options.integer( + PlayerOptions.CHAT_ANTI_SPAM_RATE_LIMIT_CAPACITY, + PluginSettings.CHAT_ANTI_SPAM_RATE_LIMIT_CAPACITY, + ).coerceAtLeast(1) + val refillIntervalMillis = options.integer( + PlayerOptions.CHAT_ANTI_SPAM_RATE_LIMIT_REFILL_INTERVAL_SECONDS, + PluginSettings.CHAT_ANTI_SPAM_RATE_LIMIT_REFILL_INTERVAL_SECONDS, + ).coerceAtLeast(1) * 1000L + val bucket = tokenBuckets.compute(playerId) { _, existing -> + existing?.takeIf { + it.capacity == capacity && it.refillIntervalMillis == refillIntervalMillis + } ?: TokenBucket(capacity, refillIntervalMillis, nowMillis) + } ?: return true + return bucket.tryConsume(nowMillis) + } + + private fun checkSimilarity(playerId: UUID, normalized: String, options: PlayerOptionView, nowMillis: Long): Result { val history = histories[playerId] ?: return Result.CLEAN - val maxAgeMillis = configuration.get(PluginSettings.CHAT_ANTI_SPAM_HISTORY_MAX_AGE_SECONDS) * 1000L + val maxAgeMillis = options.integer( + PlayerOptions.CHAT_ANTI_SPAM_HISTORY_MAX_AGE_SECONDS, + PluginSettings.CHAT_ANTI_SPAM_HISTORY_MAX_AGE_SECONDS, + ).coerceAtLeast(1) * 1000L val compared = synchronized(history.list) { history.list.removeIf { nowMillis - it.createdAtMillis > maxAgeMillis } - history.list.takeLast(configuration.get(PluginSettings.CHAT_ANTI_SPAM_SIMILAR_CHECK_AMOUNT)).toList() + history.list.takeLast( + options.integer( + PlayerOptions.CHAT_ANTI_SPAM_SIMILAR_CHECK_AMOUNT, + PluginSettings.CHAT_ANTI_SPAM_SIMILAR_CHECK_AMOUNT, + ).coerceAtLeast(1), + ).toList() } if (compared.isEmpty()) return Result.CLEAN - val maxSimilarity = configuration.get(PluginSettings.CHAT_ANTI_SPAM_SIMILAR_MAX_SIMILARITY) - val minDistance = configuration.get(PluginSettings.CHAT_ANTI_SPAM_SIMILAR_MIN_DISTANCE) + val maxSimilarity = options.decimal( + PlayerOptions.CHAT_ANTI_SPAM_SIMILAR_MAX_SIMILARITY, + PluginSettings.CHAT_ANTI_SPAM_SIMILAR_MAX_SIMILARITY, + ) + val minDistance = options.integer( + PlayerOptions.CHAT_ANTI_SPAM_SIMILAR_MIN_DISTANCE, + PluginSettings.CHAT_ANTI_SPAM_SIMILAR_MIN_DISTANCE, + ) for (entry in compared) { val distance = editDistance(normalized, entry.message) val similarity = similarity(normalized, entry.message, distance) @@ -71,8 +124,11 @@ class ChatAntiSpamService(private val configuration: PaperConfigurationService) return Result.CLEAN } - private fun remember(playerId: UUID, normalized: String, nowMillis: Long) { - val maxSize = configuration.get(PluginSettings.CHAT_ANTI_SPAM_HISTORY_SIZE) + private fun remember(playerId: UUID, normalized: String, options: PlayerOptionView, nowMillis: Long) { + val maxSize = options.integer( + PlayerOptions.CHAT_ANTI_SPAM_HISTORY_SIZE, + PluginSettings.CHAT_ANTI_SPAM_HISTORY_SIZE, + ).coerceAtLeast(1) val history = histories.compute(playerId) { _, existing -> existing?.takeIf { it.capacity == maxSize } ?: History(maxSize) } ?: return @@ -92,6 +148,31 @@ class ChatAntiSpamService(private val configuration: PaperConfigurationService) val list: EvictingRingList = EvictingRingList(capacity), ) + private class TokenBucket( + val capacity: Int, + val refillIntervalMillis: Long, + nowMillis: Long, + ) { + private var tokens = capacity.toDouble() + private var lastRefillMillis = nowMillis + + @Synchronized + fun tryConsume(nowMillis: Long): Boolean { + refill(nowMillis) + if (tokens < 1.0) return false + tokens -= 1.0 + return true + } + + private fun refill(nowMillis: Long) { + val elapsed = nowMillis - lastRefillMillis + if (elapsed <= 0L) return + + tokens = min(capacity.toDouble(), tokens + elapsed.toDouble() * capacity / refillIntervalMillis) + lastRefillMillis = nowMillis + } + } + enum class Result { CLEAN, SPAM, @@ -99,15 +180,18 @@ class ChatAntiSpamService(private val configuration: PaperConfigurationService) companion object { private val histories = ConcurrentHashMap() + private val tokenBuckets = ConcurrentHashMap() @JvmStatic fun clear(playerId: UUID) { histories.remove(playerId) + tokenBuckets.remove(playerId) } @JvmStatic fun clearAll() { histories.clear() + tokenBuckets.clear() } private fun normalize(message: String): String { diff --git a/paper/src/main/kotlin/io/wdsj/asw/bukkit/util/context/SignContext.kt b/paper/src/main/kotlin/io/wdsj/asw/bukkit/util/context/SignContext.kt index d7c876c3..69bb71ca 100644 --- a/paper/src/main/kotlin/io/wdsj/asw/bukkit/util/context/SignContext.kt +++ b/paper/src/main/kotlin/io/wdsj/asw/bukkit/util/context/SignContext.kt @@ -10,17 +10,25 @@ object SignContext { private val signEditHistory = ContextHistory() fun addMessage(player: Player, entry: SignContextEntry) { + addMessage(player, entry, contextCapacity()) + } + + fun addMessage(player: Player, entry: SignContextEntry, capacity: Int) { if (entry.content.isBlank()) return signEditHistory.removeMatching(player.uniqueId) { it.target == entry.target } - signEditHistory.add(player.uniqueId, contextCapacity(), entry) + signEditHistory.add(player.uniqueId, capacity.coerceAtLeast(1), entry) } fun getHistory(player: Player): List { + return getHistory(player, contextCapacity(), setting(PluginSettings.SIGN_CONTEXT_TIME_LIMIT)) + } + + fun getHistory(player: Player, capacity: Int, timeLimitSeconds: Int): List { return signEditHistory.snapshot( player.uniqueId, - contextCapacity(), - setting(PluginSettings.SIGN_CONTEXT_TIME_LIMIT) * 1_000L, + capacity.coerceAtLeast(1), + timeLimitSeconds.coerceAtLeast(1) * 1_000L, ) { it.time } } diff --git a/paper/src/main/resources/plugin.yml b/paper/src/main/resources/plugin.yml index 63850f9b..4072654a 100644 --- a/paper/src/main/resources/plugin.yml +++ b/paper/src/main/resources/plugin.yml @@ -30,6 +30,7 @@ permissions: advancedsensitivewords.notice: true advancedsensitivewords.update: true advancedsensitivewords.command.*: true + advancedsensitivewords.option.*: true advancedsensitivewords.bypass.*: description: Bypass all player-owned Advanced Sensitive Words filters. default: false @@ -57,6 +58,34 @@ permissions: advancedsensitivewords.command.player.reset: true advancedsensitivewords.command.player.punish: true advancedsensitivewords.command.ai.status: true + advancedsensitivewords.option.*: + description: All Advanced Sensitive Words player option overrides. + default: false + children: + advancedsensitivewords.option.chat.*: true + advancedsensitivewords.option.book.*: true + advancedsensitivewords.option.sign.*: true + advancedsensitivewords.option.anvil.*: true + advancedsensitivewords.option.item.*: true + advancedsensitivewords.option.ai.*: true + advancedsensitivewords.option.chat.*: + description: Chat, command, anti-spam, and chat AI option overrides. + default: false + advancedsensitivewords.option.book.*: + description: Book option overrides. + default: false + advancedsensitivewords.option.sign.*: + description: Sign option overrides. + default: false + advancedsensitivewords.option.anvil.*: + description: Anvil option overrides. + default: false + advancedsensitivewords.option.item.*: + description: Item option overrides. + default: false + advancedsensitivewords.option.ai.*: + description: LLM moderation option overrides. + default: false advancedsensitivewords.bypass: description: Global bypass for all player-owned Advanced Sensitive Words filters. default: false diff --git a/paper/src/test/java/io/wdsj/asw/bukkit/ai/LlmChatDetectionServiceStatusTest.java b/paper/src/test/java/io/wdsj/asw/bukkit/ai/LlmChatDetectionServiceStatusTest.java index 6c93375c..4eab416c 100644 --- a/paper/src/test/java/io/wdsj/asw/bukkit/ai/LlmChatDetectionServiceStatusTest.java +++ b/paper/src/test/java/io/wdsj/asw/bukkit/ai/LlmChatDetectionServiceStatusTest.java @@ -1,5 +1,6 @@ package io.wdsj.asw.bukkit.ai; +import io.wdsj.asw.bukkit.api.moderation.LlmModerationCategory; import io.wdsj.asw.bukkit.setting.PaperConfigurationService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -37,9 +38,9 @@ void reportsDisabledAiWithoutCreatingExecutorThreads() { assertEquals("deepseek-v4-flash", status.modelName()); assertEquals(LlmApiMode.CHAT_COMPLETIONS, status.apiMode()); LlmCategoryPolicy harassment = status.categoryPolicy().get( - io.wdsj.asw.bukkit.api.moderation.LlmModerationCategory.HARASSMENT); + LlmModerationCategory.HARASSMENT); LlmCategoryPolicy clean = status.categoryPolicy().get( - io.wdsj.asw.bukkit.api.moderation.LlmModerationCategory.CLEAN); + LlmModerationCategory.CLEAN); assertEquals(0.75D, harassment.notifyConfidence()); assertEquals(0.90D, harassment.punishConfidence()); assertEquals(-1.0D, clean.notifyConfidence());