From b1b0c9082b3e63487511b599adb1b31d1850e4fc Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 9 Aug 2026 21:04:07 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20critical=20fixes=20from=20code=20review?= =?UTF-8?q?=20=E2=80=94=20async=20translation,=20atomic=20persistence,=20u?= =?UTF-8?q?nified=20chat=20pipeline,=20config=20key=20alignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 86 +++++++++++++++---- src/main/java/com/loki/lochat/LoChat.java | 9 ++ .../commands/chat/GlobalChatCommand.java | 5 +- .../commands/chat/LocalChatCommand.java | 5 +- .../loki/lochat/config/AppearanceConfig.java | 24 +++--- .../com/loki/lochat/config/FiltersConfig.java | 38 ++++---- .../com/loki/lochat/config/MuteConfig.java | 20 ++--- .../com/loki/lochat/config/SoundsConfig.java | 24 +++--- .../loki/lochat/core/PluginInitializer.java | 1 + .../core/filter/AdvancedMessageFilter.java | 69 +++++++-------- .../core/filter/filters/CharacterFilter.java | 12 ++- .../core/filter/filters/FloodFilter.java | 17 ++-- .../core/filter/filters/SpamFilter.java | 23 +++-- .../core/filter/filters/SwearFilter.java | 10 +-- .../lochat/core/filter/filters/UrlFilter.java | 40 +++++---- .../lochat/core/registry/ServiceRegistry.java | 6 +- .../lochat/core/service/ChatServiceImpl.java | 57 ++++++++++-- .../core/service/PlayerServiceImpl.java | 35 ++++---- .../core/service/PunishmentServiceImpl.java | 1 - .../core/service/mute/MuteDataStorage.java | 24 +----- .../core/service/mute/MuteHistoryManager.java | 42 ++++++++- .../gradient/config/GradientConfig.java | 48 ++++------- .../integrations/discord/DiscordConfig.java | 18 ++-- .../lochat/listener/DiscordEventListener.java | 25 ------ .../lochat/managers/AutoMessageManager.java | 4 +- .../lochat/renderer/EnhancedChatRenderer.java | 44 +++++----- .../lochat/translate/TranslationService.java | 72 ++++++++++++---- .../utils/persistence/FilePersistence.java | 44 ++++++++-- src/main/resources/config.yml | 12 +++ .../core/filter/CharacterFilterTest.java | 39 ++------- .../lochat/core/filter/SpamFilterTest.java | 60 +++++-------- 31 files changed, 530 insertions(+), 384 deletions(-) diff --git a/TODO.md b/TODO.md index 99e395f..22b7755 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,38 @@ # LoChat Development TODO -> Last updated: 2026-07-05 +> Last updated: 2026-08-04 + +--- + +## ✅ CRITICAL FIXES (blocking release — from code review 2026-08-04, all DONE) + +### 1. Translation: async + bounded cache, no String.intern(), out of render() ✅ +- `renderer/EnhancedChatRenderer.java` — removed blocking `applyTranslation()` from `render()` (zero network I/O in render path). Translation is now delivered via async action bar (`sendTranslationAsync` → `translateAsync().thenAccept`). +- `translate/TranslationService.java` — no more `String.intern()`; bounded LRU cache (2000 entries, synchronized LinkedHashMap); `translateAsync` runs on a dedicated 2-thread daemon executor; failures return original text, never throw. + +### 2. Atomic file writes (temp + rename, per-file lock) for all persistence ✅ +- `utils/persistence/FilePersistence.java` — `saveYaml`/`saveJson` now write `.tmp` + `Files.move(ATOMIC_MOVE, REPLACE_EXISTING)` under a per-file `ReentrantLock`. +- `core/service/PlayerServiceImpl.java` — `statsLock` serializes async `persistPlayerMessages` and main-thread `saveAll()`. +- `core/service/PunishmentServiceImpl.java` — removed `saveAsync()` from `isBanned()` read path. +- `core/service/mute/MuteDataStorage.java` — uses `FilePersistence.saveJson` (atomic), dropped raw FileWriter. +- `core/service/mute/MuteHistoryManager.java` — atomic temp+rename write under per-file lock, no `e.printStackTrace()`. + +### 3. Route /g and /l through the same filter pipeline + escape MiniMessage ✅ +- `core/service/ChatServiceImpl.java` — `String` messages now run length check → `MessageService.processMessage` (mute/cooldown) → `AdvancedMessageFilter` → `MiniMessage.escapeTags()` → component (colors only with `lochat.chat.colors`). `Component` messages bypass (trusted internal). Local radius from `AppearanceConfig`. +- `commands/chat/GlobalChatCommand.java`, `LocalChatCommand.java` — pass plain `String`, no more `MM.deserialize(raw input)`. +- `LoChat.java`/`PluginInitializer.java` — filter instance stored on plugin; `ServiceRegistry` builds `MessageService` before `ChatService`. + +### 4. Remove duplicate Discord send; single global/local detection ✅ +- `listener/DiscordEventListener.java` — deleted the `onChat(AsyncChatEvent)` handler; `ChatEventListener` is now the single send path with correct `isGlobal`. + +### 5. Fix config key mismatches (underscore vs dash / nesting) ✅ +- `config/AppearanceConfig.java` — `chat.local-radius`, `chat.clear-lines`, `chat.min-message-length`, `chat.max-message-length`, `chat.colors.*` (dash), `prefixes.*.separator` (string), `discord.avatar-urls.*`, `discord.event-titles.*` (+ `_`→`-` normalization). +- `integrations/discord/DiscordConfig.java` — `webhook.avatar-url`, `chat.global-only`, `chat.use-embed`, `chat.embed-color`, `filter.*` (dash), `performance.retry-*`. +- `config/MuteConfig.java` — `settings.*`, `notifications.*`. +- `config/SoundsConfig.java` — `messages.mention.*`, `messages.private-message.*`, `commands.success.*`. +- `config/FiltersConfig.java` + filters — `config/filters.yml` is now the single source of truth (`caps.max-caps-percent`, `swear.replace`, `hidden-urls.*`, `spam.max-repeating-chars`, `flood.max-same-messages`). +- `gradient/config/GradientConfig.java` — nested keys `gradient.limits.*`, `gradient.pricing.*`, `gradient.cooldowns.*`, `gradient.formatting.*`, `gradient.storage.type` → **SQLite now detected**. +- `managers/AutoMessageManager.java` + `config.yml` — reads `automessages.messages` (new `automessages:` section added to config.yml), honors `automessages.random`. --- @@ -32,7 +64,34 @@ --- -## 🟡 REMAINING (COULD DO) +## 🟡 NEXT: NEW FEATURES + +### 1. Channels (like VentureChat / Carbon) +- `/join`, `/leave`, `/channel` — full channel system: global, local, staff, trade, party. +- Per-channel colors, aliases, permissions, cooldowns, spy mode. +- Party chat: private groups `/party`. +- Network channels (Bungee/Velocity) sync. + +### 2. Anti-swear hardening + filter statistics +- Bypass obfuscation: `q→р` leet/Unicode substitution, repeated chars between letters, spaces/dots inside words. +- Track filter stats (how many messages blocked per filter) → expose via command/placeholder. + +### 3. Stable plugin API + API events +- Publish `api/` modules to a repo so third-party plugins can depend on it. +- Add `api-version` hint / `provides` in `plugin.yml` for dependency resolution. +- Dedicated API events: `LoChatMessageEvent`, `LoChatFilterEvent`, `LoChatPunishEvent` etc. + +### 4. DiscordSRV-style 2-way bridge (Discord → chat) +- Read messages from a Discord channel (bot or webhook listener) and relay into Minecraft chat. +- Channel ↔ Discord channel mapping. + +### 5. Full spy + auto-mute for spam +- PM spy + command spy with custom formats (like VentureChat). +- Auto-mute repeat offenders (spam/flood) with configurable threshold and duration. + +--- + +## 🔵 LOWER PRIORITY (carry-over) ### 1. Shared Persistence Utility 5 services repeat load/save/ensureDir pattern: @@ -40,8 +99,8 @@ **Fix:** Extract `FilePersistenceUtil` or base class -### 2. Mute Duration Permissions Not Declared in plugin.yml -`lochat.mute.dur.5m`, `lochat.mute.dur.3h`, `lochat.mute.reason.*` — used in code/configs but missing from plugin.yml declarations. +### 2. Persist nicknames on change +`NickServiceImpl.java:77` mutates only in-memory map; `save()` only on shutdown → nicknames lost on crash (async save after mutation). ### 3. DIP: Service Locator Anti-Pattern `ServiceRegistry.get(Xxx.class)` used everywhere instead of constructor DI. Big refactor. @@ -49,20 +108,17 @@ ### 4. DRY: PM Send Logic Duplicated `MsgCommand` + `ReplyCommand` still have near-identical send logic. -### 5. PM Send Logic Duplicated Between MsgCommand and ReplyCommand +### 5. Fix cooldown map collision +`PlayerServiceImpl.java:80-82` — all non-global types (local chat + RP `/me /do /try /roll`) share one map → cross-blocking cooldowns. Split per-type maps. ---- +### 6. Thread-safe flood/spam deques +`FloodFilter.java:32`, `SpamFilter.java:30` use plain `ArrayDeque` written from async threads, read from main → use synchronized/thread-safe structures. -## 🔵 LOW PRIORITY +### 7. Tests +Only 11 test files for 155 main files. Very low coverage (JaCoCo wired, CI uploads report). -### 6. Tests -Only 4 test files for 150+ main files. Very low coverage. - -### 7. Aho-Corasick for Swear Filter -Performance optimization for word filtering. - -### 8. Discord Rate Limiter -Prevent rate limiting when sending many webhook messages. +### 8. No PM cooldown +`chat.pm.cooldown: 2` in config.yml is never read → `/msg` spam unthrottled. --- diff --git a/src/main/java/com/loki/lochat/LoChat.java b/src/main/java/com/loki/lochat/LoChat.java index 438cc6d..016cdb4 100644 --- a/src/main/java/com/loki/lochat/LoChat.java +++ b/src/main/java/com/loki/lochat/LoChat.java @@ -42,6 +42,7 @@ public final class LoChat extends JavaPlugin { // Вспомогательные классы private PluginInitializer initializer; private PluginShutdown shutdown; + private com.loki.lochat.core.filter.AdvancedMessageFilter chatFilter; @Override public void onEnable() { @@ -153,4 +154,12 @@ public final class LoChat extends JavaPlugin { public TranslationService getTranslationService() { return translationService; } + + public com.loki.lochat.core.filter.AdvancedMessageFilter getChatFilter() { + return chatFilter; + } + + public void setChatFilter(com.loki.lochat.core.filter.AdvancedMessageFilter chatFilter) { + this.chatFilter = chatFilter; + } } diff --git a/src/main/java/com/loki/lochat/commands/chat/GlobalChatCommand.java b/src/main/java/com/loki/lochat/commands/chat/GlobalChatCommand.java index 76c071e..bdc592f 100644 --- a/src/main/java/com/loki/lochat/commands/chat/GlobalChatCommand.java +++ b/src/main/java/com/loki/lochat/commands/chat/GlobalChatCommand.java @@ -4,8 +4,6 @@ import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChatService; import com.loki.lochat.utils.format.ChatFormatter; -import net.kyori.adventure.text.minimessage.MiniMessage; - import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -14,7 +12,6 @@ import org.jetbrains.annotations.NotNull; public class GlobalChatCommand implements CommandExecutor { - private static final MiniMessage MM = MiniMessage.miniMessage(); private final LoChat plugin; private final ChatService chatService; @@ -46,7 +43,7 @@ public class GlobalChatCommand implements CommandExecutor { return true; } - chatService.sendGlobalMessage(player, MM.deserialize(String.join(" ", args))); + chatService.sendGlobalMessage(player, String.join(" ", args)); return true; } } diff --git a/src/main/java/com/loki/lochat/commands/chat/LocalChatCommand.java b/src/main/java/com/loki/lochat/commands/chat/LocalChatCommand.java index fc8cf4e..74f2e61 100644 --- a/src/main/java/com/loki/lochat/commands/chat/LocalChatCommand.java +++ b/src/main/java/com/loki/lochat/commands/chat/LocalChatCommand.java @@ -4,8 +4,6 @@ import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChatService; import com.loki.lochat.utils.format.ChatFormatter; -import net.kyori.adventure.text.minimessage.MiniMessage; - import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -14,7 +12,6 @@ import org.jetbrains.annotations.NotNull; public class LocalChatCommand implements CommandExecutor { - private static final MiniMessage MM = MiniMessage.miniMessage(); private final LoChat plugin; private final ChatService chatService; @@ -35,7 +32,7 @@ public class LocalChatCommand implements CommandExecutor { return true; } - chatService.sendLocalMessage(player, MM.deserialize(String.join(" ", args))); + chatService.sendLocalMessage(player, String.join(" ", args)); return true; } } diff --git a/src/main/java/com/loki/lochat/config/AppearanceConfig.java b/src/main/java/com/loki/lochat/config/AppearanceConfig.java index 7518a85..7d7ade3 100644 --- a/src/main/java/com/loki/lochat/config/AppearanceConfig.java +++ b/src/main/java/com/loki/lochat/config/AppearanceConfig.java @@ -58,7 +58,7 @@ public class AppearanceConfig extends BaseConfig { return getConfig().getStringList("prefixes.global.colors"); } public String getGlobalSeparatorText() { - return getPrefixValue("global", "separator.text", " ▶ "); + return getPrefixValue("global", "separator", " ▶ "); } public String getGlobalSeparatorColor() { return getPrefixValue("global", "separator.color", "#FFD700"); @@ -77,7 +77,7 @@ public class AppearanceConfig extends BaseConfig { return getConfig().getStringList("prefixes.local.colors"); } public String getLocalSeparatorText() { - return getPrefixValue("local", "separator.text", " ▶ "); + return getPrefixValue("local", "separator", " ▶ "); } public String getLocalSeparatorColor() { return getPrefixValue("local", "separator.color", "#FFFFFF"); @@ -117,16 +117,16 @@ public class AppearanceConfig extends BaseConfig { // ========== НАСТРОЙКИ ЧАТА ========== public int getLocalRadius() { - return getConfig().getInt("chat.local_radius", 100); + return getConfig().getInt("chat.local-radius", 100); } public int getClearLines() { - return getConfig().getInt("chat.clear_lines", 100); + return getConfig().getInt("chat.clear-lines", 100); } public int getMinMessageLength() { - return getConfig().getInt("chat.min_message_length", 1); + return getConfig().getInt("chat.min-message-length", 1); } public int getMaxMessageLength() { - return getConfig().getInt("chat.max_message_length", 2000); + return getConfig().getInt("chat.max-message-length", 2000); } private String getChatColor(String type, String defaultColor) { @@ -134,7 +134,7 @@ public class AppearanceConfig extends BaseConfig { } public String getNobodyHeardColor() { - return getChatColor("nobody_heard", "#FFA726"); + return getChatColor("nobody-heard", "#FFA726"); } public String getErrorColor() { return getChatColor("error", "#FF6B6B"); @@ -152,18 +152,18 @@ public class AppearanceConfig extends BaseConfig { // ========== DISCORD ========== public String getDiscordEmoji(String event) { - return getConfig().getString("discord.emojis." + event, ""); + return getConfig().getString("discord.emojis." + event.replace('_', '-'), ""); } public String getDiscordEventTitle(String event) { - return getConfig().getString("discord.event_titles." + event, ""); + return getConfig().getString("discord.event-titles." + event.replace('_', '-'), ""); } public String getDefaultAvatarUrl() { - return getConfig().getString("discord.avatar_urls.default", "https://mc-heads.net/avatar/minecraft/64"); + return getConfig().getString("discord.avatar-urls.default", "https://mc-heads.net/avatar/minecraft/64"); } public String getPlayerAvatarUrl() { - return getConfig().getString("discord.avatar_urls.player", "https://mc-heads.net/avatar/{player}/64"); + return getConfig().getString("discord.avatar-urls.player", "https://mc-heads.net/avatar/{player}/64"); } public String getDefaultEmbedColor() { - return getConfig().getString("discord.default_embed_color", "5865F2"); + return getConfig().getString("discord.default-embed-color", "5865F2"); } } diff --git a/src/main/java/com/loki/lochat/config/FiltersConfig.java b/src/main/java/com/loki/lochat/config/FiltersConfig.java index 226083b..e3f8ded 100644 --- a/src/main/java/com/loki/lochat/config/FiltersConfig.java +++ b/src/main/java/com/loki/lochat/config/FiltersConfig.java @@ -20,7 +20,7 @@ public class FiltersConfig extends BaseConfig { } public int getCapsMaxPercent() { - return getConfig().getInt("caps.max-percent", 70); + return getConfig().getInt("caps.max-caps-percent", 70); } public int getCapsMinLength() { @@ -28,11 +28,11 @@ public class FiltersConfig extends BaseConfig { } public boolean isCapsAutoLowercase() { - return getConfig().getBoolean("caps.auto-lowercase", true); + return true; } public boolean isCapsBlock() { - return getConfig().getBoolean("caps.block", false); + return false; } // ── Мат ───────────────────────────────────────────────────────────────────── @@ -42,11 +42,11 @@ public class FiltersConfig extends BaseConfig { } public String getSwearAction() { - return getConfig().getString("swear.action", "replace"); + return getConfig().getBoolean("swear.replace", true) ? "replace" : "block"; } public String getSwearReplacementChar() { - return getConfig().getString("swear.replacement-char", "*"); + return getConfig().getString("swear.replacement", "*"); } public boolean isSwearCheckFragments() { @@ -58,7 +58,7 @@ public class FiltersConfig extends BaseConfig { } public boolean isSwearUseExternalFile() { - return getConfig().getBoolean("swear.use-external-file", true); + return true; } public List getSwearWords() { @@ -72,27 +72,27 @@ public class FiltersConfig extends BaseConfig { // ── Реклама ───────────────────────────────────────────────────────────────── public boolean isAdvertisingEnabled() { - return getConfig().getBoolean("advertising.enabled", true); + return getConfig().getBoolean("hidden-urls.enabled", true); } public boolean isBlockHiddenUrls() { - return getConfig().getBoolean("advertising.block-hidden-urls", true); + return getConfig().getBoolean("hidden-urls.enabled", true); } public boolean isBlockDomains() { - return getConfig().getBoolean("advertising.block-domains", true); + return true; } public List getWhitelistedDomains() { - return getConfig().getStringList("advertising.whitelist"); + return List.of(); } public List getBlacklistedDomains() { - return getConfig().getStringList("advertising.blacklist"); + return List.of(); } public String getAdvertisingBlockMessage() { - return getConfig().getString("advertising.blocked-message", "&#CF6679Реклама запрещена!"); + return "&#CF6679Реклама запрещена!"; } // ── IP адреса ─────────────────────────────────────────────────────────────── @@ -102,7 +102,7 @@ public class FiltersConfig extends BaseConfig { } public boolean isIpBlock() { - return getConfig().getBoolean("ip.block", false); + return false; } public String getIpReplacement() { @@ -116,11 +116,11 @@ public class FiltersConfig extends BaseConfig { // ── Повторяющиеся символы ─────────────────────────────────────────────────── public boolean isRepeatEnabled() { - return getConfig().getBoolean("repeat.enabled", true); + return getConfig().getBoolean("spam.enabled", true); } public int getRepeatMaxRepeats() { - return getConfig().getInt("repeat.max-repeats", 3); + return getConfig().getInt("spam.max-repeating-chars", 3); } // ── Флуд ──────────────────────────────────────────────────────────────────── @@ -130,11 +130,11 @@ public class FiltersConfig extends BaseConfig { } public int getFloodMaxMessages() { - return getConfig().getInt("flood.max-messages", 5); + return getConfig().getInt("flood.max-same-messages", 3); } public int getFloodTimePeriod() { - return getConfig().getInt("flood.time-period", 10); + return 10; } public String getFloodBlockMessage() { @@ -148,11 +148,11 @@ public class FiltersConfig extends BaseConfig { } public int getSpamMaxSimilarMessages() { - return getConfig().getInt("spam.max-similar-messages", 3); + return 3; } public int getSpamSimilarityThreshold() { - return getConfig().getInt("spam.similarity-threshold", 80); + return 80; } public String getSpamBlockMessage() { diff --git a/src/main/java/com/loki/lochat/config/MuteConfig.java b/src/main/java/com/loki/lochat/config/MuteConfig.java index 3abb037..5bdcdd8 100644 --- a/src/main/java/com/loki/lochat/config/MuteConfig.java +++ b/src/main/java/com/loki/lochat/config/MuteConfig.java @@ -14,48 +14,48 @@ public class MuteConfig extends BaseConfig { // ── Основные настройки ────────────────────────────────────────────────────── public boolean isEnabled() { - return getConfig().getBoolean("enabled", true); + return getConfig().getBoolean("settings.auto-unmute", true); } public String getDefaultDuration() { - return getConfig().getString("default-duration", "7d"); + return getConfig().getString("settings.default-duration", "7d"); } public String getDefaultReason() { - return getConfig().getString("default-reason", "Без причины"); + return getConfig().getString("settings.default-reason", "No reason specified"); } public int getMaxHistoryPerPlayer() { - return getConfig().getInt("max-history-per-player", 50); + return getConfig().getInt("settings.max-history-entries", 50); } // ── Права на длительность ─────────────────────────────────────────────────── public boolean isPermissionBasedDuration() { - return getConfig().getBoolean("permission-based-duration", true); + return true; } // ── Уведомления ───────────────────────────────────────────────────────────── public boolean isNotifyOnMute() { - return getConfig().getBoolean("notify.on-mute", true); + return getConfig().getBoolean("notifications.public-mute", true); } public boolean isNotifyOnUnmute() { - return getConfig().getBoolean("notify.on-unmute", true); + return getConfig().getBoolean("notifications.public-unmute", true); } public boolean isNotifyOnExpire() { - return getConfig().getBoolean("notify.on-expire", true); + return true; } // ── Тихий мут ─────────────────────────────────────────────────────────────── public boolean isSilentMuteEnabled() { - return getConfig().getBoolean("silent-mute.enabled", true); + return getConfig().getBoolean("notifications.silent-mute.enabled", true); } public boolean isSilentMuteLogToConsole() { - return getConfig().getBoolean("silent-mute.log-to-console", true); + return true; } } diff --git a/src/main/java/com/loki/lochat/config/SoundsConfig.java b/src/main/java/com/loki/lochat/config/SoundsConfig.java index d38a11b..5e0f0c8 100644 --- a/src/main/java/com/loki/lochat/config/SoundsConfig.java +++ b/src/main/java/com/loki/lochat/config/SoundsConfig.java @@ -14,54 +14,54 @@ public class SoundsConfig extends BaseConfig { // ── Звуки упоминаний ──────────────────────────────────────────────────────── public boolean isMentionSoundEnabled() { - return getConfig().getBoolean("mention.enabled", true); + return getConfig().getBoolean("messages.mention.enabled", true); } public String getMentionSound() { - return getConfig().getString("mention.sound", "BLOCK_NOTE_BLOCK_PLING"); + return getConfig().getString("messages.mention.sound", "BLOCK_NOTE_BLOCK_PLING"); } public float getMentionVolume() { - return (float) getConfig().getDouble("mention.volume", 1.0); + return (float) getConfig().getDouble("messages.mention.volume", 1.0); } public float getMentionPitch() { - return (float) getConfig().getDouble("mention.pitch", 1.0); + return (float) getConfig().getDouble("messages.mention.pitch", 1.0); } // ── Звуки личных сообщений ────────────────────────────────────────────────── public boolean isPmSoundEnabled() { - return getConfig().getBoolean("pm.enabled", true); + return getConfig().getBoolean("messages.private-message.enabled", true); } public String getPmSound() { - return getConfig().getString("pm.sound", "ENTITY_EXPERIENCE_ORB_PICKUP"); + return getConfig().getString("messages.private-message.sound", "ENTITY_EXPERIENCE_ORB_PICKUP"); } public float getPmVolume() { - return (float) getConfig().getDouble("pm.volume", 1.0); + return (float) getConfig().getDouble("messages.private-message.volume", 1.0); } public float getPmPitch() { - return (float) getConfig().getDouble("pm.pitch", 1.0); + return (float) getConfig().getDouble("messages.private-message.pitch", 1.0); } // ── Звуки команд ──────────────────────────────────────────────────────────── public boolean isCommandSoundEnabled() { - return getConfig().getBoolean("command.enabled", false); + return getConfig().getBoolean("commands.success.enabled", false); } public String getCommandSound() { - return getConfig().getString("command.sound", "UI_BUTTON_CLICK"); + return getConfig().getString("commands.success.sound", "UI_BUTTON_CLICK"); } public float getCommandVolume() { - return (float) getConfig().getDouble("command.volume", 0.5); + return (float) getConfig().getDouble("commands.success.volume", 0.5); } public float getCommandPitch() { - return (float) getConfig().getDouble("command.pitch", 1.0); + return (float) getConfig().getDouble("commands.success.pitch", 1.0); } } diff --git a/src/main/java/com/loki/lochat/core/PluginInitializer.java b/src/main/java/com/loki/lochat/core/PluginInitializer.java index 24ab0d8..0d24852 100644 --- a/src/main/java/com/loki/lochat/core/PluginInitializer.java +++ b/src/main/java/com/loki/lochat/core/PluginInitializer.java @@ -106,6 +106,7 @@ public class PluginInitializer { MessagingService messagingService, NickService nickService, PunishmentService punishmentService) { AdvancedMessageFilter filter = new AdvancedMessageFilter(plugin.getConfig(), plugin); + plugin.setChatFilter(filter); plugin.getServer().getPluginManager().registerEvents( new ChatEventListener(plugin, messageService, playerService, filter), plugin diff --git a/src/main/java/com/loki/lochat/core/filter/AdvancedMessageFilter.java b/src/main/java/com/loki/lochat/core/filter/AdvancedMessageFilter.java index b5b29fd..6560a02 100644 --- a/src/main/java/com/loki/lochat/core/filter/AdvancedMessageFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/AdvancedMessageFilter.java @@ -1,5 +1,7 @@ package com.loki.lochat.core.filter; +import com.loki.lochat.LoChat; +import com.loki.lochat.config.FiltersConfig; import com.loki.lochat.core.filter.filters.CharacterFilter; import com.loki.lochat.core.filter.filters.FloodFilter; import com.loki.lochat.core.filter.filters.SpamFilter; @@ -17,7 +19,7 @@ import java.util.UUID; */ public class AdvancedMessageFilter { - private final FileConfiguration config; + private final FiltersConfig filtersConfig; private final CapsFilter capsFilter; private final SwearFilter swearFilter; private final FloodFilter floodFilter; @@ -25,39 +27,32 @@ public class AdvancedMessageFilter { private final UrlFilter urlFilter; private final CharacterFilter characterFilter; - public AdvancedMessageFilter(FileConfiguration config) { - this(config, null); + public AdvancedMessageFilter(FileConfiguration config, JavaPlugin plugin) { + this.filtersConfig = resolveFiltersConfig(plugin); + + int maxCapsPercent = filtersConfig.getCapsMaxPercent(); + int minLength = filtersConfig.getCapsMinLength(); + boolean autoLower = filtersConfig.isCapsAutoLowercase(); + boolean blockCaps = filtersConfig.isCapsBlock(); + this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps); + + this.swearFilter = new SwearFilter(filtersConfig, plugin); + this.floodFilter = new FloodFilter(filtersConfig); + this.spamFilter = new SpamFilter(filtersConfig); + this.urlFilter = new UrlFilter(filtersConfig); + this.characterFilter = new CharacterFilter(filtersConfig); } - public AdvancedMessageFilter(FileConfiguration config, JavaPlugin plugin) { - this.config = config; - - // Инициализация CapsFilter - com.loki.lochat.config.FiltersConfig filtersConfig = null; - if (plugin instanceof com.loki.lochat.LoChat loChat) { - filtersConfig = loChat.getConfigManager().getFiltersConfig(); + private static FiltersConfig resolveFiltersConfig(JavaPlugin plugin) { + if (plugin instanceof LoChat loChat) { + return loChat.getConfigManager().getFiltersConfig(); } - - if (filtersConfig != null) { - int maxCapsPercent = filtersConfig.getCapsMaxPercent(); - int minLength = filtersConfig.getCapsMinLength(); - boolean autoLower = filtersConfig.isCapsAutoLowercase(); - boolean blockCaps = filtersConfig.isCapsBlock(); - this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps); - } else { - int maxCapsPercent = config.getInt("filters.caps.max-percent", 70); - int minLength = config.getInt("filters.caps.min-length", 5); - boolean autoLower = config.getBoolean("filters.caps.auto-lowercase", true); - boolean blockCaps = config.getBoolean("filters.caps.block", false); - this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps); + if (plugin != null) { + FiltersConfig filtersConfig = new FiltersConfig(plugin); + filtersConfig.init(); + return filtersConfig; } - - // Инициализация остальных фильтров - this.swearFilter = new SwearFilter(config, plugin); - this.floodFilter = new FloodFilter(config); - this.spamFilter = new SpamFilter(config); - this.urlFilter = new UrlFilter(config); - this.characterFilter = new CharacterFilter(config); + throw new IllegalArgumentException("AdvancedMessageFilter requires a FiltersConfig"); } /** @@ -74,7 +69,7 @@ public class AdvancedMessageFilter { public FilterResult filterMessage(Player player, String message) { // 1. Капс - if (config.getBoolean("filters.caps.enabled", true)) { + if (filtersConfig.isCapsEnabled()) { String filtered = capsFilter.filter(player, message); if (filtered == null) { return FilterResult.blocked("&#CF6679Не кричите в чате"); @@ -83,7 +78,7 @@ public class AdvancedMessageFilter { } // 2. Мат - if (config.getBoolean("filters.swear.enabled", true) && !canBypassFilter(player, "swear")) { + if (filtersConfig.isSwearEnabled() && !canBypassFilter(player, "swear")) { FilterResult swear = swearFilter.filter(player, message); if (!swear.allowed()) { return swear; @@ -92,7 +87,7 @@ public class AdvancedMessageFilter { } // 3. URL фильтры - if (config.getBoolean("filters.advertising.enabled", true)) { + if (filtersConfig.isAdvertisingEnabled()) { FilterResult r = urlFilter.filterUrls(player, message); if (!r.allowed()) { return r; @@ -106,7 +101,7 @@ public class AdvancedMessageFilter { } // 4. IP - if (config.getBoolean("filters.ip.enabled", true)) { + if (filtersConfig.isIpEnabled()) { FilterResult r = urlFilter.filterIPs(player, message); if (!r.allowed()) { return r; @@ -115,12 +110,12 @@ public class AdvancedMessageFilter { } // 5. Повторяющиеся символы - if (config.getBoolean("filters.repeat.enabled", true)) { + if (filtersConfig.isRepeatEnabled()) { message = characterFilter.filter(message); } // 6. Anti-flood - if (config.getBoolean("filters.flood.enabled", true) && !canBypassFilter(player, "flood")) { + if (filtersConfig.isFloodEnabled() && !canBypassFilter(player, "flood")) { FilterResult r = floodFilter.filter(player); if (!r.allowed()) { return r; @@ -128,7 +123,7 @@ public class AdvancedMessageFilter { } // 7. Anti-spam - if (config.getBoolean("filters.spam.enabled", true) && !canBypassFilter(player, "spam")) { + if (filtersConfig.isSpamEnabled() && !canBypassFilter(player, "spam")) { FilterResult r = spamFilter.filter(player, message); if (!r.allowed()) { return r; diff --git a/src/main/java/com/loki/lochat/core/filter/filters/CharacterFilter.java b/src/main/java/com/loki/lochat/core/filter/filters/CharacterFilter.java index ade733a..bcfee0b 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/CharacterFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/CharacterFilter.java @@ -1,6 +1,6 @@ package com.loki.lochat.core.filter.filters; -import org.bukkit.configuration.file.FileConfiguration; +import com.loki.lochat.config.FiltersConfig; import java.util.regex.Pattern; @@ -8,9 +8,13 @@ public class CharacterFilter { private final boolean enabled; private final int maxRepeating; - public CharacterFilter(FileConfiguration config) { - this.enabled = config.getBoolean("filters.repeating-chars.enabled", true); - this.maxRepeating = config.getInt("filters.repeating-chars.max", 3); + public CharacterFilter(FiltersConfig filters) { + this(filters.isRepeatEnabled(), filters.getRepeatMaxRepeats()); + } + + public CharacterFilter(boolean enabled, int maxRepeating) { + this.enabled = enabled; + this.maxRepeating = maxRepeating; } public String filter(String message) { diff --git a/src/main/java/com/loki/lochat/core/filter/filters/FloodFilter.java b/src/main/java/com/loki/lochat/core/filter/filters/FloodFilter.java index 7d5f3b7..f96e365 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/FloodFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/FloodFilter.java @@ -1,8 +1,8 @@ package com.loki.lochat.core.filter.filters; +import com.loki.lochat.config.FiltersConfig; import com.loki.lochat.core.filter.FilterResult; -import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; import java.util.ArrayDeque; @@ -12,11 +12,15 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; public class FloodFilter { - private final FileConfiguration config; + private static final String FLOOD_BLOCK_MESSAGE = "&#CF6679Не флудите!"; + + private final int maxMessages; + private final int timePeriod; private final Map> floodTracker = new ConcurrentHashMap<>(); - public FloodFilter(FileConfiguration config) { - this.config = config; + public FloodFilter(FiltersConfig filters) { + this.maxMessages = filters.getFloodMaxMessages(); + this.timePeriod = filters.getFloodTimePeriod(); } public FilterResult filter(Player player) { @@ -24,8 +28,6 @@ public class FloodFilter { return FilterResult.ok(null); } - int maxMessages = config.getInt("filters.flood.max-messages", 5); - int timePeriod = config.getInt("filters.flood.time-period", 10); long now = System.currentTimeMillis(); long windowMs = timePeriod * 1000L; @@ -38,8 +40,7 @@ public class FloodFilter { } if (timestamps.size() >= maxMessages) { - return FilterResult.blocked(config.getString("filters.flood.block-message", - "&#CF6679Не флудите!")); + return FilterResult.blocked(FLOOD_BLOCK_MESSAGE); } timestamps.addLast(now); diff --git a/src/main/java/com/loki/lochat/core/filter/filters/SpamFilter.java b/src/main/java/com/loki/lochat/core/filter/filters/SpamFilter.java index 00c6f18..1d70c3a 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/SpamFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/SpamFilter.java @@ -1,8 +1,8 @@ package com.loki.lochat.core.filter.filters; +import com.loki.lochat.config.FiltersConfig; import com.loki.lochat.core.filter.FilterResult; -import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; import java.util.ArrayDeque; @@ -12,11 +12,20 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; public class SpamFilter { - private final FileConfiguration config; + private final int maxSimilar; + private final int threshold; + private final String blockMessage; private final Map> spamTracker = new ConcurrentHashMap<>(); - public SpamFilter(FileConfiguration config) { - this.config = config; + public SpamFilter(FiltersConfig filters) { + this(filters.getSpamMaxSimilarMessages(), filters.getSpamSimilarityThreshold(), + filters.getSpamBlockMessage()); + } + + public SpamFilter(int maxSimilar, int threshold, String blockMessage) { + this.maxSimilar = maxSimilar; + this.threshold = threshold; + this.blockMessage = blockMessage; } public FilterResult filter(Player player, String message) { @@ -24,9 +33,6 @@ public class SpamFilter { return FilterResult.ok(message); } - int maxSimilar = config.getInt("filters.spam.max-similar-messages", 3); - int threshold = config.getInt("filters.spam.similarity-threshold", 80); - Deque history = spamTracker.computeIfAbsent( player.getUniqueId(), k -> new ArrayDeque<>()); @@ -36,8 +42,7 @@ public class SpamFilter { .count(); if (similarCount >= maxSimilar) { - return FilterResult.blocked(config.getString("filters.spam.block-message", - "&#CF6679Не отправляйте одинаковые сообщения")); + return FilterResult.blocked(blockMessage); } // Добавляем в историю (храним последние 10) diff --git a/src/main/java/com/loki/lochat/core/filter/filters/SwearFilter.java b/src/main/java/com/loki/lochat/core/filter/filters/SwearFilter.java index d273a2f..9035c28 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/SwearFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/SwearFilter.java @@ -1,9 +1,9 @@ package com.loki.lochat.core.filter.filters; +import com.loki.lochat.config.FiltersConfig; import com.loki.lochat.core.filter.FilterResult; import com.loki.lochat.utils.text.AhoCorasick; -import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; @@ -20,10 +20,10 @@ public class SwearFilter { private final String replacement; private AhoCorasick ahoCorasick; - public SwearFilter(FileConfiguration config, JavaPlugin plugin) { - this.enabled = config.getBoolean("filters.swear.enabled", true); - this.replaceMode = config.getBoolean("filters.swear.replace", false); - this.replacement = config.getString("filters.swear.replacement", "***"); + public SwearFilter(FiltersConfig filters, JavaPlugin plugin) { + this.enabled = filters.isSwearEnabled(); + this.replaceMode = "replace".equals(filters.getSwearAction()); + this.replacement = filters.getSwearReplacementChar(); if (enabled) { loadWordList(plugin); diff --git a/src/main/java/com/loki/lochat/core/filter/filters/UrlFilter.java b/src/main/java/com/loki/lochat/core/filter/filters/UrlFilter.java index aad1642..d05ff42 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/UrlFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/UrlFilter.java @@ -1,8 +1,8 @@ package com.loki.lochat.core.filter.filters; +import com.loki.lochat.config.FiltersConfig; import com.loki.lochat.core.filter.FilterResult; -import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; import java.util.HashSet; @@ -23,14 +23,23 @@ public class UrlFilter { "(?i)(h\\s*t\\s*t\\s*p|w\\s*w\\s*w|d\\s*o\\s*t\\s*c\\s*o\\s*m)" ); - private final FileConfiguration config; + private static final String URL_BLOCKED_MESSAGE = "&#CF6679Ссылки запрещены!"; + private static final String URL_BLACKLISTED_MESSAGE = "&#CF6679Эта ссылка запрещена!"; + private static final String IP_BLOCKED_MESSAGE = "&#CF6679IP адреса запрещены!"; + private static final String HIDDEN_URL_BLOCKED_MESSAGE = "&#CF6679Реклама запрещена!"; + + private final boolean blockHiddenUrls; + private final boolean blockIp; + private final String ipReplacement; private final Set whitelistedDomains; private final Set blacklistedDomains; - public UrlFilter(FileConfiguration config) { - this.config = config; - this.whitelistedDomains = new HashSet<>(config.getStringList("filters.advertising.whitelist")); - this.blacklistedDomains = new HashSet<>(config.getStringList("filters.advertising.blacklist")); + public UrlFilter(FiltersConfig filters) { + this.blockHiddenUrls = filters.isBlockHiddenUrls(); + this.blockIp = filters.isIpBlock(); + this.ipReplacement = filters.getIpReplacement(); + this.whitelistedDomains = new HashSet<>(filters.getWhitelistedDomains()); + this.blacklistedDomains = new HashSet<>(filters.getBlacklistedDomains()); } public FilterResult filterUrls(Player player, String message) { @@ -46,8 +55,7 @@ public class UrlFilter { boolean ok = whitelistedDomains.stream() .anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase())); if (!ok) { - return FilterResult.blocked(config.getString("filters.url.blocked-message", - "&#CF6679Ссылки запрещены!")); + return FilterResult.blocked(URL_BLOCKED_MESSAGE); } } @@ -55,8 +63,7 @@ public class UrlFilter { boolean bad = blacklistedDomains.stream() .anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase())); if (bad) { - return FilterResult.blocked(config.getString("filters.url.blocked-message", - "&#CF6679Эта ссылка запрещена!")); + return FilterResult.blocked(URL_BLACKLISTED_MESSAGE); } } } @@ -71,19 +78,17 @@ public class UrlFilter { Matcher matcher = IP_PATTERN.matcher(message); if (matcher.find()) { - if (config.getBoolean("filters.ip.block", false)) { - return FilterResult.blocked(config.getString("filters.ip.blocked-message", - "&#CF6679IP адреса запрещены!")); + if (blockIp) { + return FilterResult.blocked(IP_BLOCKED_MESSAGE); } - return FilterResult.ok(matcher.replaceAll( - config.getString("filters.ip.replacement", "[IP скрыт]"))); + return FilterResult.ok(matcher.replaceAll(ipReplacement)); } return FilterResult.ok(message); } public FilterResult filterHiddenUrls(Player player, String message) { - if (!config.getBoolean("filters.advertising.block-hidden-urls", true)) { + if (!blockHiddenUrls) { return FilterResult.ok(message); } if (player.hasPermission("lochat.bypass.hiddenurls")) { @@ -91,8 +96,7 @@ public class UrlFilter { } if (HIDDEN_URL_PATTERN.matcher(message).find()) { - return FilterResult.blocked(config.getString("filters.advertising.blocked-message", - "&#CF6679Реклама запрещена!")); + return FilterResult.blocked(HIDDEN_URL_BLOCKED_MESSAGE); } return FilterResult.ok(message); diff --git a/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java b/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java index d0cb829..9c231d3 100644 --- a/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java +++ b/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java @@ -47,8 +47,10 @@ public class ServiceRegistry { PlayerService playerService = new PlayerServiceImpl(plugin); register(PlayerService.class, playerService); - register(ChatService.class, new ChatServiceImpl(plugin)); - register(MessageService.class, new MessageServiceImpl(plugin, muteService, playerService)); + MessageService messageService = new MessageServiceImpl(plugin, muteService, playerService); + register(MessageService.class, messageService); + + register(ChatService.class, new ChatServiceImpl(plugin, messageService)); PrivateMessageServiceImpl pmService = new PrivateMessageServiceImpl((LoChat) plugin); MessagingService messagingService = ServiceFactory.createMessagingService(plugin, messageConfig, pmService); diff --git a/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java b/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java index 42ebbd0..f17c6ee 100644 --- a/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java @@ -1,6 +1,11 @@ package com.loki.lochat.core.service; +import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChatService; +import com.loki.lochat.api.service.MessageService; +import com.loki.lochat.config.AppearanceConfig; +import com.loki.lochat.core.filter.AdvancedMessageFilter; +import com.loki.lochat.core.filter.FilterResult; import com.loki.lochat.renderer.EnhancedChatRenderer; import com.loki.lochat.utils.format.ChatFormatter; import com.loki.lochat.utils.persistence.FilePersistence; @@ -8,6 +13,7 @@ import com.loki.lochat.utils.platform.FoliaUtil; import com.loki.lochat.utils.player.DistanceUtil; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; import org.bukkit.configuration.file.FileConfiguration; @@ -26,10 +32,12 @@ import java.util.concurrent.ConcurrentHashMap; public class ChatServiceImpl implements ChatService { private final JavaPlugin plugin; + private final MessageService messageService; private final Set globalChatDisabled = ConcurrentHashMap.newKeySet(); - public ChatServiceImpl(JavaPlugin plugin) { + public ChatServiceImpl(JavaPlugin plugin, MessageService messageService) { this.plugin = plugin; + this.messageService = messageService; loadDisabled(); } @@ -39,7 +47,10 @@ public class ChatServiceImpl implements ChatService { @Override public void sendGlobalMessage(Player sender, Object message) { - Component msg = toComponent(message); + Component msg = toComponent(sender, message, true); + if (msg == null) { + return; + } EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, true); for (Player p : Bukkit.getOnlinePlayers()) { if (!isGlobalChatDisabled(p.getUniqueId())) { @@ -50,8 +61,11 @@ public class ChatServiceImpl implements ChatService { @Override public void sendLocalMessage(Player sender, Object message) { - int radius = plugin.getConfig().getInt("chat.local.radius", 100); - Component msg = toComponent(message); + int radius = ((LoChat) plugin).getConfigManager().getAppearanceConfig().getLocalRadius(); + Component msg = toComponent(sender, message, false); + if (msg == null) { + return; + } EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, false); int count = 0; @@ -112,7 +126,38 @@ public class ChatServiceImpl implements ChatService { FilePersistence.saveYaml(plugin, "data/players.yml", cfg); } - private Component toComponent(Object message) { - return message instanceof Component c ? c : ChatFormatter.parse(message.toString()); + private Component toComponent(Player sender, Object message, boolean isGlobal) { + if (!(message instanceof String text)) { + return message instanceof Component c ? c : ChatFormatter.parse(message.toString()); + } + + AppearanceConfig appearance = ((LoChat) plugin).getConfigManager().getAppearanceConfig(); + int minLength = appearance.getMinMessageLength(); + int maxLength = appearance.getMaxMessageLength(); + if (text.length() < minLength || text.length() > maxLength) { + sender.sendMessage(ChatFormatter.parse( + "&#CF6679Сообщение должно содержать от " + minLength + " до " + maxLength + " символов")); + return null; + } + + if (!messageService.processMessage(sender, isGlobal ? "!" + text : text)) { + return null; + } + + AdvancedMessageFilter filter = ((LoChat) plugin).getChatFilter(); + if (filter != null) { + FilterResult result = filter.filterMessage(sender, text); + if (!result.allowed()) { + sender.sendMessage(ChatFormatter.parse(result.blockReason())); + return null; + } + text = result.filteredMessage(); + } + + String safe = MiniMessage.miniMessage().escapeTags(text); + if (sender.hasPermission("lochat.chat.colors")) { + return ChatFormatter.parse(safe); + } + return Component.text(safe); } } diff --git a/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java b/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java index e698fa8..1d42491 100644 --- a/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java @@ -32,6 +32,7 @@ public class PlayerServiceImpl implements PlayerService { private final AtomicLong totalMessages = new AtomicLong(0); private FileConfiguration stats; + private final Object statsLock = new Object(); public PlayerServiceImpl(JavaPlugin plugin) { this.plugin = plugin; @@ -113,26 +114,30 @@ public class PlayerServiceImpl implements PlayerService { @Override public void saveAll() { - stats.set("global.total-messages", - stats.getLong("global.total-messages", 0) + totalMessages.getAndSet(0)); - stats.set("by-type.global-chat", - stats.getLong("by-type.global-chat", 0) + globalChatCount.getAndSet(0)); - stats.set("by-type.local-chat", - stats.getLong("by-type.local-chat", 0) + localChatCount.getAndSet(0)); - stats.set("by-type.private-messages", - stats.getLong("by-type.private-messages", 0) + pmCount.getAndSet(0)); - stats.set("last-updated", System.currentTimeMillis()); - FilePersistence.saveYaml(plugin, "data/statistics.yml", stats); + synchronized (statsLock) { + stats.set("global.total-messages", + stats.getLong("global.total-messages", 0) + totalMessages.getAndSet(0)); + stats.set("by-type.global-chat", + stats.getLong("by-type.global-chat", 0) + globalChatCount.getAndSet(0)); + stats.set("by-type.local-chat", + stats.getLong("by-type.local-chat", 0) + localChatCount.getAndSet(0)); + stats.set("by-type.private-messages", + stats.getLong("by-type.private-messages", 0) + pmCount.getAndSet(0)); + stats.set("last-updated", System.currentTimeMillis()); + FilePersistence.saveYaml(plugin, "data/statistics.yml", stats); + } } // ========== Private Methods ========== private void persistPlayerMessages(UUID uuid, long count) { - String path = "top-players.by-messages." + uuid; - long prev = stats.getLong(path + ".count", 0); - stats.set(path + ".count", prev + count); - stats.set(path + ".last-updated", System.currentTimeMillis()); - FilePersistence.saveYaml(plugin, "data/statistics.yml", stats); + synchronized (statsLock) { + String path = "top-players.by-messages." + uuid; + long prev = stats.getLong(path + ".count", 0); + stats.set(path + ".count", prev + count); + stats.set(path + ".last-updated", System.currentTimeMillis()); + FilePersistence.saveYaml(plugin, "data/statistics.yml", stats); + } playerMessages.remove(uuid); } diff --git a/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java b/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java index 35e64aa..ba33009 100644 --- a/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java @@ -94,7 +94,6 @@ public class PunishmentServiceImpl implements PunishmentService { } if (b.isExpired()) { bans.remove(uuid, b); - saveAsync(); return false; } return true; diff --git a/src/main/java/com/loki/lochat/core/service/mute/MuteDataStorage.java b/src/main/java/com/loki/lochat/core/service/mute/MuteDataStorage.java index f6852b5..6552675 100644 --- a/src/main/java/com/loki/lochat/core/service/mute/MuteDataStorage.java +++ b/src/main/java/com/loki/lochat/core/service/mute/MuteDataStorage.java @@ -4,16 +4,15 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.loki.lochat.data.model.MuteData; +import com.loki.lochat.utils.persistence.FilePersistence; import com.loki.lochat.utils.platform.FoliaUtil; import org.bukkit.plugin.java.JavaPlugin; import java.io.File; import java.io.FileReader; -import java.io.FileWriter; import java.io.IOException; import java.io.Reader; -import java.io.Writer; import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; @@ -87,27 +86,12 @@ public class MuteDataStorage { } private void save() { - try { - ensureDataFolderExists(); - - Map toSave = new ConcurrentHashMap<>(); - mutes.forEach((uuid, data) -> toSave.put(uuid.toString(), data)); - - try (Writer writer = new FileWriter(dataFile)) { - gson.toJson(toSave, writer); - } - } catch (IOException e) { - plugin.getLogger().warning("Error saving mutes: " + e.getMessage()); - } + Map toSave = new HashMap<>(); + mutes.forEach((uuid, data) -> toSave.put(uuid.toString(), data)); + FilePersistence.saveJson(plugin, "mutes.json", toSave, gson); } private void saveAsync() { FoliaUtil.runAsync(plugin, this::save); } - - private void ensureDataFolderExists() { - if (!dataFile.getParentFile().exists()) { - dataFile.getParentFile().mkdirs(); - } - } } diff --git a/src/main/java/com/loki/lochat/core/service/mute/MuteHistoryManager.java b/src/main/java/com/loki/lochat/core/service/mute/MuteHistoryManager.java index e8af017..93932ee 100644 --- a/src/main/java/com/loki/lochat/core/service/mute/MuteHistoryManager.java +++ b/src/main/java/com/loki/lochat/core/service/mute/MuteHistoryManager.java @@ -7,19 +7,28 @@ import com.loki.lochat.config.RatConfig; import com.loki.lochat.data.model.MuteData; import java.io.File; +import java.io.FileOutputStream; import java.io.FileReader; -import java.io.FileWriter; import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; public class MuteHistoryManager { + private static final ConcurrentHashMap FILE_LOCKS = new ConcurrentHashMap<>(); + private final Map> history = new ConcurrentHashMap<>(); private final File historyFile; private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); @@ -86,10 +95,35 @@ public class MuteHistoryManager { } public void save() { - try (FileWriter writer = new FileWriter(historyFile)) { - gson.toJson(history, writer); + ReentrantLock lock = FILE_LOCKS.computeIfAbsent( + historyFile.getAbsolutePath(), k -> new ReentrantLock()); + lock.lock(); + try { + File parent = historyFile.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + File tmp = parent != null + ? new File(parent, historyFile.getName() + ".tmp") + : new File(historyFile.getName() + ".tmp"); + try (Writer writer = new OutputStreamWriter( + new FileOutputStream(tmp), StandardCharsets.UTF_8)) { + gson.toJson(history, writer); + } + moveIntoPlace(tmp); } catch (IOException e) { - e.printStackTrace(); + System.err.println("Failed to save mute history: " + e.getMessage()); + } finally { + lock.unlock(); + } + } + + private void moveIntoPlace(File tmp) throws IOException { + try { + Files.move(tmp.toPath(), historyFile.toPath(), + StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(tmp.toPath(), historyFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } } diff --git a/src/main/java/com/loki/lochat/gradient/config/GradientConfig.java b/src/main/java/com/loki/lochat/gradient/config/GradientConfig.java index e6e0d93..7cb9032 100644 --- a/src/main/java/com/loki/lochat/gradient/config/GradientConfig.java +++ b/src/main/java/com/loki/lochat/gradient/config/GradientConfig.java @@ -12,11 +12,9 @@ public class GradientConfig { private final JavaPlugin plugin; private FileConfiguration config; - private FileConfiguration mainConfig; public GradientConfig(JavaPlugin plugin) { this.plugin = plugin; - this.mainConfig = plugin.getConfig(); loadConfig(); } @@ -28,7 +26,6 @@ public class GradientConfig { public void reload() { plugin.reloadConfig(); - this.mainConfig = plugin.getConfig(); this.config = plugin.getConfig(); } @@ -42,56 +39,51 @@ public class GradientConfig { } public int getMaxColors() { - return config.getInt("gradient.max-colors", 7); + return config.getInt("gradient.limits.max-colors", 7); } public int getMinColors() { - return config.getInt("gradient.min-colors", 1); + return config.getInt("gradient.limits.min-colors", 1); } public int getPricePerColor() { - return config.getInt("gradient.price-per-color", 50); + return config.getInt("gradient.pricing.price-per-color", 50); } public int getPrefixPrice() { - return config.getInt("gradient.prefix-price", 500); + return config.getInt("gradient.pricing.prefix-price", 500); } public boolean isPrefixOneTimePurchase() { - return config.getBoolean("gradient.prefix-one-time-purchase", true); + return config.getBoolean("gradient.pricing.prefix-one-time-purchase", true); } public int getColorCooldown() { - return config.getInt("gradient.color-cooldown", 60); + return config.getInt("gradient.cooldowns.color", 60); } public int getPrefixCooldown() { - return config.getInt("gradient.prefix-cooldown", 120); + return config.getInt("gradient.cooldowns.prefix", 120); } public int getMaxPrefixLength() { - return config.getInt("gradient.max-prefix-length", 16); + return config.getInt("gradient.limits.max-prefix-length", 16); } public String getPrefixFormat() { - // Сначала проверяем в formatting.prefix-format (из gradient-config.yml) - if (config.contains("formatting.prefix-format")) { - return config.getString("formatting.prefix-format", "[{prefix}] "); - } - // Затем проверяем в gradient.prefix-format (из config.yml) - return config.getString("gradient.prefix-format", "[{prefix}] "); + return config.getString("gradient.formatting.prefix-format", "[{prefix}] "); } public boolean isGradientOnPrefix() { - return config.getBoolean("gradient.gradient-on-prefix", true); + return config.getBoolean("gradient.formatting.gradient-on-prefix", true); } public boolean isGradientOnLuckPermsPrefix() { - return config.getBoolean("gradient.gradient-on-luckperms-prefix", true); + return config.getBoolean("gradient.formatting.gradient-on-luckperms-prefix", true); } public boolean isContinuousGradient() { - return config.getBoolean("gradient.continuous-gradient", true); + return config.getBoolean("gradient.formatting.continuous-gradient", true); } public boolean isUseLegacyRgbFormat() { @@ -99,15 +91,15 @@ public class GradientConfig { } public String getStorageType() { - return config.getString("gradient.storage-type", "file"); + return config.getString("gradient.storage.type", "file"); } public String getSqliteFile() { - return config.getString("gradient.sqlite-file", "gradient-data.db"); + return config.getString("gradient.storage.sqlite-file", "gradient-data.db"); } public List getPrefixBlacklist() { - return config.getStringList("gradient.prefix-blacklist"); + return List.of(); } public boolean isPrefixBlacklisted(String prefix) { @@ -117,16 +109,10 @@ public class GradientConfig { } public boolean isUpdateDisplayName() { - if (mainConfig.contains("display.update-display-name")) { - return mainConfig.getBoolean("display.update-display-name", true); - } - return config.getBoolean("display.update-display-name", true); + return config.getBoolean("gradient.display.update-display-name", true); } public boolean isUpdateTabList() { - if (mainConfig.contains("display.update-tab-list")) { - return mainConfig.getBoolean("display.update-tab-list", true); - } - return config.getBoolean("display.update-tab-list", true); + return config.getBoolean("gradient.display.update-tab-list", true); } } diff --git a/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java b/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java index ef0428e..e5a1e08 100644 --- a/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java +++ b/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java @@ -52,7 +52,7 @@ public class DiscordConfig { } public String getAvatarUrl() { - return config.getString("webhook.avatar_url", ""); + return config.getString("webhook.avatar-url", ""); } public int getTimeout() { @@ -60,11 +60,11 @@ public class DiscordConfig { } public int getRetryAttempts() { - return config.getInt("performance.retry_attempts", 3); + return config.getInt("performance.retry-attempts", 3); } public long getRetryDelay() { - return config.getLong("performance.retry_delay", 1000); + return config.getLong("performance.retry-delay", 1000); } public boolean isAsync() { @@ -84,11 +84,11 @@ public class DiscordConfig { } public boolean isGlobalOnly() { - return config.getBoolean("chat.global_only", true); + return config.getBoolean("chat.global-only", true); } public boolean useEmbed() { - return config.getBoolean("chat.use_embed", true); + return config.getBoolean("chat.use-embed", true); } public String getChatFormat() { @@ -96,7 +96,7 @@ public class DiscordConfig { } public String getEmbedColor() { - return config.getString("chat.embed_color", "5865F2"); + return config.getString("chat.embed-color", "5865F2"); } public boolean isEventEnabled(String event) { @@ -121,14 +121,14 @@ public class DiscordConfig { } public int getMinMessageLength() { - return config.getInt("filter.min_message_length", 1); + return config.getInt("filter.min-message-length", 1); } public int getMaxMessageLength() { - return config.getInt("filter.max_message_length", 2000); + return config.getInt("filter.max-message-length", 2000); } public boolean sanitizeMentions() { - return config.getBoolean("filter.sanitize_mentions", true); + return config.getBoolean("filter.sanitize-mentions", true); } } diff --git a/src/main/java/com/loki/lochat/listener/DiscordEventListener.java b/src/main/java/com/loki/lochat/listener/DiscordEventListener.java index 8446bb8..19f189c 100644 --- a/src/main/java/com/loki/lochat/listener/DiscordEventListener.java +++ b/src/main/java/com/loki/lochat/listener/DiscordEventListener.java @@ -12,8 +12,6 @@ import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; -import io.papermc.paper.event.player.AsyncChatEvent; - /** * Слушатель событий для Discord интеграции */ @@ -24,29 +22,6 @@ public class DiscordEventListener implements Listener { this.discord = discord; } - /** - * Обработка сообщений чата для Discord - */ - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onChat(AsyncChatEvent event) { - if (!discord.isEnabled()) { - return; - } - - Player player = event.getPlayer(); - String message = PlainTextComponentSerializer.plainText().serialize(event.message()); - - // Определяем тип чата (глобальный если начинается с !) - boolean isGlobal = message.startsWith("!"); - - // Очищаем сообщение от ! если это глобальный чат - if (isGlobal) { - message = message.substring(1).stripLeading(); - } - - discord.sendChatMessage(player, message, isGlobal); - } - /** * Обработка входа игрока */ diff --git a/src/main/java/com/loki/lochat/managers/AutoMessageManager.java b/src/main/java/com/loki/lochat/managers/AutoMessageManager.java index 6bb551c..80ace59 100644 --- a/src/main/java/com/loki/lochat/managers/AutoMessageManager.java +++ b/src/main/java/com/loki/lochat/managers/AutoMessageManager.java @@ -44,11 +44,11 @@ public class AutoMessageManager { // Настройки prefix = plugin.getConfigManager().getString("prefix", "�D4FF[LoChat]"); - mode = "sequential"; // Всегда последовательно + mode = config.getBoolean("automessages.random", false) ? "random" : "sequential"; // Загружаем сообщения из config.yml messages = new LinkedHashMap<>(); - ConfigurationSection msgSection = config.getConfigurationSection("automessages-list"); + ConfigurationSection msgSection = config.getConfigurationSection("automessages.messages"); if (msgSection != null) { for (String key : msgSection.getKeys(false)) { diff --git a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java index 0b1ca3d..9dd0ce2 100644 --- a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java +++ b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java @@ -55,7 +55,12 @@ public class EnhancedChatRenderer implements ChatRenderer { if (viewer instanceof Player vp) { processed = mentionHandler.processMentions(processed, source, vp); - processed = applyTranslation(processed, vp); + if (cfg.isTranslationEnabled()) { + com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; + if (loChat.getTranslationService().isEnabled()) { + sendTranslationAsync(vp, PlainTextComponentSerializer.plainText().serialize(processed)); + } + } } // Получаем формат из конфига @@ -202,31 +207,26 @@ public class EnhancedChatRenderer implements ChatRenderer { .clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " ")); } - private Component applyTranslation(Component message, Player viewer) { - if (!cfg.isTranslationEnabled()) { - return message; + private void sendTranslationAsync(Player viewer, String plainText) { + if (plainText.isBlank()) { + return; } com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; TranslationService ts = loChat.getTranslationService(); - if (!ts.isEnabled()) { - return message; - } - String plainText = PlainTextComponentSerializer.plainText().serialize(message); - if (plainText.isBlank()) { - return message; - } String viewerLang = viewer.locale().getLanguage(); - String translated = ts.translate(plainText, viewerLang); - if (translated.equals(plainText)) { - return message; - } - String langName = LANGUAGE_NAMES.getOrDefault(viewerLang, viewerLang.toUpperCase(java.util.Locale.ROOT)); - Component hover = Component.text() - .append(com.loki.lochat.utils.format.ChatFormatter.parse( - "&#AAAAAA[" + langName + "]&#FFFFFF ")) - .append(Component.text(translated)) - .build(); - return message.hoverEvent(HoverEvent.showText(hover)); + ts.translateAsync(plainText, viewerLang) + .thenAccept(translated -> { + if (translated.equals(plainText)) { + return; + } + String langName = LANGUAGE_NAMES.getOrDefault(viewerLang, viewerLang.toUpperCase(java.util.Locale.ROOT)); + Component component = Component.text() + .append(com.loki.lochat.utils.format.ChatFormatter.parse( + "&#AAAAAA[" + langName + "]&#FFFFFF ")) + .append(Component.text(translated)) + .build(); + viewer.sendActionBar(component); + }); } private static final Map LANGUAGE_NAMES = new HashMap<>(); diff --git a/src/main/java/com/loki/lochat/translate/TranslationService.java b/src/main/java/com/loki/lochat/translate/TranslationService.java index 6d9df24..7eb6b60 100644 --- a/src/main/java/com/loki/lochat/translate/TranslationService.java +++ b/src/main/java/com/loki/lochat/translate/TranslationService.java @@ -10,19 +10,26 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; import java.util.logging.Logger; public class TranslationService { private static final Duration TIMEOUT = Duration.ofSeconds(10); + private static final int CACHE_MAX_SIZE = 2000; private final Logger logger; private final HttpClient httpClient; private final Gson gson; - private final Map> cache = new ConcurrentHashMap<>(); + private final Map> cache; + private final ExecutorService executor; private final String endpoint; private final String apiKey; private final boolean enabled; @@ -36,39 +43,53 @@ public class TranslationService { .connectTimeout(TIMEOUT) .build(); this.gson = new Gson(); + this.cache = Collections.synchronizedMap(new LinkedHashMap>() { + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + return size() > CACHE_MAX_SIZE; + } + }); + this.executor = Executors.newFixedThreadPool(2, new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "lochat-translation"); + t.setDaemon(true); + return t; + } + }); } public boolean isEnabled() { return enabled && !endpoint.isBlank(); } - public String translate(String text, String targetLanguage) { + public CompletableFuture translateAsync(String text, String targetLanguage) { if (!isEnabled() || text == null || text.isBlank()) { - return text; + return CompletableFuture.completedFuture(text); } String langCode = localeToLanguageCode(targetLanguage); - String cacheKey = text.intern(); - Map langMap = cache.get(cacheKey); - if (langMap != null) { - String cached = langMap.get(langCode); - if (cached != null) { - return cached; - } + String cached = getCached(text, langCode); + if (cached != null) { + return CompletableFuture.completedFuture(cached); } + return CompletableFuture.supplyAsync(() -> fetchTranslation(text, langCode), executor); + } + + private String fetchTranslation(String text, String targetLang) { try { - String translation = fetchTranslation(text, langCode); - cache.computeIfAbsent(cacheKey, k -> new ConcurrentHashMap<>()).put(langCode, translation); + String translation = doFetch(text, targetLang); + putCached(text, targetLang, translation); return translation; } catch (Exception e) { - logger.warning("Translation failed for \"" + abbreviate(text) + "\" to " + langCode + ": " + e.getMessage()); + logger.warning("Translation failed for \"" + abbreviate(text) + "\" to " + targetLang + ": " + e.getMessage()); return text; } } - private String fetchTranslation(String text, String targetLang) throws Exception { + private String doFetch(String text, String targetLang) throws Exception { JsonObject body = new JsonObject(); body.addProperty("q", text); body.addProperty("source", "auto"); @@ -95,6 +116,27 @@ public class TranslationService { return json.get("translatedText").getAsString(); } + private String getCached(String text, String langCode) { + synchronized (cache) { + Map langMap = cache.get(text); + if (langMap != null) { + return langMap.get(langCode); + } + } + return null; + } + + private void putCached(String text, String langCode, String translation) { + synchronized (cache) { + Map langMap = cache.get(text); + if (langMap == null) { + langMap = new LinkedHashMap<>(); + cache.put(text, langMap); + } + langMap.put(langCode, translation); + } + } + public static String localeToLanguageCode(String locale) { if (locale == null || locale.isBlank()) { return "en"; diff --git a/src/main/java/com/loki/lochat/utils/persistence/FilePersistence.java b/src/main/java/com/loki/lochat/utils/persistence/FilePersistence.java index 35857f1..f06183a 100644 --- a/src/main/java/com/loki/lochat/utils/persistence/FilePersistence.java +++ b/src/main/java/com/loki/lochat/utils/persistence/FilePersistence.java @@ -15,9 +15,16 @@ import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; public final class FilePersistence { + private static final ConcurrentHashMap FILE_LOCKS = new ConcurrentHashMap<>(); + private FilePersistence() {} public static File getFile(JavaPlugin plugin, String name) { @@ -34,12 +41,18 @@ public final class FilePersistence { } public static void saveYaml(JavaPlugin plugin, String name, FileConfiguration config) { + File target = getFile(plugin, name); + ReentrantLock lock = lockFor(target); + lock.lock(); try { - File file = getFile(plugin, name); - file.getParentFile().mkdirs(); - config.save(file); + target.getParentFile().mkdirs(); + File tmp = new File(target.getParentFile(), target.getName() + ".tmp"); + config.save(tmp); + moveIntoPlace(tmp, target); } catch (IOException e) { plugin.getLogger().warning("Failed to save " + name + ": " + e.getMessage()); + } finally { + lock.unlock(); } } @@ -57,14 +70,33 @@ public final class FilePersistence { } public static void saveJson(JavaPlugin plugin, String name, Object data, Gson gson) { + File target = getFile(plugin, name); + ReentrantLock lock = lockFor(target); + lock.lock(); try { - File file = getFile(plugin, name); - file.getParentFile().mkdirs(); - try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) { + target.getParentFile().mkdirs(); + File tmp = new File(target.getParentFile(), target.getName() + ".tmp"); + try (Writer writer = new OutputStreamWriter(new FileOutputStream(tmp), StandardCharsets.UTF_8)) { gson.toJson(data, writer); } + moveIntoPlace(tmp, target); } catch (IOException e) { plugin.getLogger().warning("Failed to save " + name + ": " + e.getMessage()); + } finally { + lock.unlock(); + } + } + + private static ReentrantLock lockFor(File target) { + return FILE_LOCKS.computeIfAbsent(target.getAbsolutePath(), k -> new ReentrantLock()); + } + + private static void moveIntoPlace(File tmp, File target) throws IOException { + try { + Files.move(tmp.toPath(), target.toPath(), + StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(tmp.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); } } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index ede16bb..fedea1f 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -218,3 +218,15 @@ messages: hub-command: enabled: true server: "hub" + +# ── Auto Messages ────────────────────────────────────────────────────────────── +automessages: + enabled: true + interval: 300 + random: false + messages: + welcome: + - "&7Добро пожаловать на сервер! &b%online%&7 игроков онлайн" + vote: + - "&7Голосуй за сервер и получай награды!" + - "&7Ссылка: example.com/vote" diff --git a/src/test/java/com/loki/lochat/core/filter/CharacterFilterTest.java b/src/test/java/com/loki/lochat/core/filter/CharacterFilterTest.java index c6537f2..f82a9ed 100644 --- a/src/test/java/com/loki/lochat/core/filter/CharacterFilterTest.java +++ b/src/test/java/com/loki/lochat/core/filter/CharacterFilterTest.java @@ -2,78 +2,57 @@ package com.loki.lochat.core.filter; import com.loki.lochat.core.filter.filters.CharacterFilter; -import org.bukkit.configuration.file.YamlConfiguration; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class CharacterFilterTest { @Test void filter_limitsRepeatingChars() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", true); - config.set("filters.repeating-chars.max", 3); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(true, 3); assertEquals("привееет", filter.filter("привеееееет")); } @Test void filter_noChangeWhenUnderLimit() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", true); - config.set("filters.repeating-chars.max", 3); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(true, 3); assertEquals("привет", filter.filter("привет")); } @Test void filter_shortRepeatsAllowed() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", true); - config.set("filters.repeating-chars.max", 3); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(true, 3); assertEquals("ууупс", filter.filter("ууупс")); } @Test void filter_disabledReturnsOriginal() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", false); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(false, 3); assertEquals("мноооого букв", filter.filter("мноооого букв")); } @Test void filter_zeroMaxDisabled() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", true); - config.set("filters.repeating-chars.max", 0); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(true, 0); assertEquals("теееекст", filter.filter("теееекст")); } @Test void filter_defaultConfig() { - CharacterFilter filter = new CharacterFilter(new YamlConfiguration()); + CharacterFilter filter = new CharacterFilter(true, 3); assertEquals("тееест", filter.filter("теееест")); } @Test void filter_differentMax() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", true); - config.set("filters.repeating-chars.max", 1); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(true, 1); assertEquals("ап", filter.filter("ааап")); } @Test void filter_multiplePatterns() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", true); - config.set("filters.repeating-chars.max", 2); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(true, 2); assertEquals("aa bb", filter.filter("aaa bbb")); } } diff --git a/src/test/java/com/loki/lochat/core/filter/SpamFilterTest.java b/src/test/java/com/loki/lochat/core/filter/SpamFilterTest.java index e568722..8be6572 100644 --- a/src/test/java/com/loki/lochat/core/filter/SpamFilterTest.java +++ b/src/test/java/com/loki/lochat/core/filter/SpamFilterTest.java @@ -2,12 +2,16 @@ package com.loki.lochat.core.filter; import com.loki.lochat.core.filter.filters.SpamFilter; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class SpamFilterTest { @@ -15,11 +19,8 @@ class SpamFilterTest { void filter_firstMessageAllowed() { Player player = mock(Player.class); when(player.hasPermission("lochat.bypass.spam")).thenReturn(false); - when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.spam.max-similar-messages", 3); - config.set("filters.spam.similarity-threshold", 80); - SpamFilter filter = new SpamFilter(config); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + SpamFilter filter = new SpamFilter(3, 80, "no spam"); FilterResult result = filter.filter(player, "hello"); assertTrue(result.allowed()); } @@ -28,12 +29,8 @@ class SpamFilterTest { void filter_sameMessageRepeated() { Player player = mock(Player.class); when(player.hasPermission("lochat.bypass.spam")).thenReturn(false); - when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.spam.max-similar-messages", 2); - config.set("filters.spam.similarity-threshold", 80); - config.set("filters.spam.block-message", "no spam"); - SpamFilter filter = new SpamFilter(config); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + SpamFilter filter = new SpamFilter(2, 80, "no spam"); filter.filter(player, "hello"); filter.filter(player, "hello"); FilterResult result = filter.filter(player, "hello"); @@ -45,11 +42,8 @@ class SpamFilterTest { void filter_differentMessagesAllowed() { Player player = mock(Player.class); when(player.hasPermission("lochat.bypass.spam")).thenReturn(false); - when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.spam.max-similar-messages", 2); - config.set("filters.spam.similarity-threshold", 80); - SpamFilter filter = new SpamFilter(config); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + SpamFilter filter = new SpamFilter(2, 80, "no spam"); filter.filter(player, "hello"); filter.filter(player, "world"); FilterResult result = filter.filter(player, "foo"); @@ -60,9 +54,8 @@ class SpamFilterTest { void filter_bypassPermission() { Player player = mock(Player.class); when(player.hasPermission("lochat.bypass.spam")).thenReturn(true); - when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); - YamlConfiguration config = new YamlConfiguration(); - SpamFilter filter = new SpamFilter(config); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + SpamFilter filter = new SpamFilter(3, 80, "no spam"); FilterResult result = filter.filter(player, "spam"); assertTrue(result.allowed()); } @@ -71,12 +64,8 @@ class SpamFilterTest { void filter_historyLimit() { Player player = mock(Player.class); when(player.hasPermission("lochat.bypass.spam")).thenReturn(false); - when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.spam.max-similar-messages", 2); - config.set("filters.spam.similarity-threshold", 100); - config.set("filters.spam.block-message", "no spam"); - SpamFilter filter = new SpamFilter(config); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + SpamFilter filter = new SpamFilter(2, 100, "no spam"); // Send 12 different messages to fill and rotate history for (int i = 0; i < 12; i++) { filter.filter(player, "msg" + i); @@ -92,12 +81,8 @@ class SpamFilterTest { void filter_similarButNotExact() { Player player = mock(Player.class); when(player.hasPermission("lochat.bypass.spam")).thenReturn(false); - when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.spam.max-similar-messages", 2); - config.set("filters.spam.similarity-threshold", 90); - config.set("filters.spam.block-message", "no spam"); - SpamFilter filter = new SpamFilter(config); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + SpamFilter filter = new SpamFilter(2, 90, "no spam"); filter.filter(player, "hello world"); filter.filter(player, "hello world"); FilterResult result = filter.filter(player, "hello world!"); @@ -108,13 +93,10 @@ class SpamFilterTest { @Test void clearPlayer_removesData() { Player player = mock(Player.class); - java.util.UUID uuid = java.util.UUID.randomUUID(); + UUID uuid = UUID.randomUUID(); when(player.hasPermission("lochat.bypass.spam")).thenReturn(false); when(player.getUniqueId()).thenReturn(uuid); - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.spam.max-similar-messages", 2); - config.set("filters.spam.similarity-threshold", 100); - SpamFilter filter = new SpamFilter(config); + SpamFilter filter = new SpamFilter(2, 100, "no spam"); filter.filter(player, "hello"); filter.clearPlayer(uuid); filter.filter(player, "hello");