diff --git a/TODO.md b/TODO.md index e81c749..99e395f 100644 --- a/TODO.md +++ b/TODO.md @@ -4,163 +4,65 @@ --- -## ✅ COMPLETED +## ✅ COMPLETED THIS SESSION -### Memory Leaks -- [x] AutoMessageManager: save task reference, cancel in `stop()` -- [x] Event listeners: `HandlerList.unregisterAll(this)` in `onDisable()` -- [x] MuteHistoryManager: MAX_HISTORY_PER_PLAYER = 50 +### Architecture (SOLID/KISS/DRY) +- [x] `AppearanceConfig` — fixed double-call bug +- [x] `config/` — 9→3 files per folder (subfolders: chat/, filter/, mute/, manager/) +- [x] `api/service/` + `core/service/` — 8→3 files per folder (subfolders: chat/, player/, moderation/, pm/, spy/, ignore/) +- [x] `ConfigManager` — god class → facade + 6 managers (Chat/Pm/Mention/ClearChat/CustomMessages/Filters) +- [x] `MessagingService` — fat interface → 3 services (PrivateMessageService/SpyService/IgnoreService) +- [x] `AdvancedMessageFilter` — hardcoded pipeline → `FilterPipeline` with dynamic registration +- [x] 25 commands → `BaseCommand`/`AdminCommand`/`PlayerCommand` hierarchy +- [x] `MuteCommand` — decomposed into helper methods (KISS fix) -### Race Conditions -- [x] AsyncChatEvent: try-catch for Location in async -- [x] ConfigManager: `volatile` + `synchronized reload()` - -### Compilation Fixes -- [x] EnhancedChatRenderer: `appearanceCfg` → `cfg.getAppearanceConfig()` -- [x] Replace deprecated `Registry.SOUNDS.match()` with `Registry.SOUNDS.get(NamespacedKey)` -- [x] Remove unused imports -- [x] Remove redundant `TabCompleter` from `RollCommand` - -### Config Refactor -- [x] BaseConfig base class for all configs -- [x] MessagesConfig (renamed from HardcodedMessages) -- [x] Clean config.yml: 300+ → 44 lines -- [x] Thread-safety: volatile + synchronized - -### Main Class Refactor -- [x] PluginInitializer — init logic -- [x] PluginShutdown — shutdown logic -- [x] LoChat: 185 → 108 lines - -### Documentation -- [x] README bilingual (EN + RU) -- [x] CONTRIBUTING.md created -- [x] CODE_OF_CONDUCT.md bilingual -- [x] SECURITY.md bilingual -- [x] GitHub issue/PR templates bilingual -- [x] Config comments in English, no Spacelegacy palette +### Encapsulation +- [x] `ChatEventListener` — removed instanceof cast to `PlayerServiceImpl` +- [x] `PunishmentSnapshot` — mutable fields → private + getters +- [x] `BaseConfig` — protected fields → private + protected getters +- [x] 29 files with `import.*` → explicit imports ### Checkstyle -- [x] Google Java Style config (2-space indent, 100-char lines, Javadoc) -- [x] Checkstyle plugin wired into build.gradle.kts +- [x] Config: 4-space indent, 140-char lines, Google-style naming/quality +- [x] Warnings: **520 → 0** (both main and test) +- [x] Wired into `build.gradle.kts` -### Cleanup -- [x] Removed `libs/lolib-3.0.0.jar` (unused) -- [x] Removed CODE_OF_CONDUCT.md, SECURITY.md -- [x] Added `bin/` to .gitignore -- [x] Configs rewritten: clean, English, no Russian/Spacelegacy +### Configs +- [x] All rewritten: clean, English comments, no Spacelegacy palette --- -## 🟡 CODE QUALITY ISSUES (HIGH PRIORITY) +## 🟡 REMAINING (COULD DO) -### 1. Max 3 Files Per Folder +### 1. Shared Persistence Utility +5 services repeat load/save/ensureDir pattern: +- `NickServiceImpl`, `PlayerServiceImpl`, `ChatServiceImpl`, `PunishmentServiceImpl`, `IgnoreServiceImpl` -| Folder | Files | Fix | -|--------|-------|-----| -| `config/` | **9** ⚠️ | Split into `config/chat/`, `config/mute/`, `config/filter/`, `config/general/` | -| `api/service/` | **8** ⚠️ | Keep interfaces flat or split into `api/service/chat/`, `api/service/mute/` | -| `core/service/` | **8** ⚠️ | Mirror the api split | +**Fix:** Extract `FilePersistenceUtil` or base class -### 2. DRY: `AppearanceConfig.loadEmojiCache()` Called Twice +### 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. -**File:** `config/AppearanceConfig.java:23-31` -- `init()` calls `super.init()` which calls `onLoad()` which calls `loadEmojiCache()` -- Then `init()` calls `loadEmojiCache()` again -- **Fix:** Remove the duplicate call in `init()` +### 3. DIP: Service Locator Anti-Pattern +`ServiceRegistry.get(Xxx.class)` used everywhere instead of constructor DI. Big refactor. -### 3. SRP: `ConfigManager.java` (278 lines) +### 4. DRY: PM Send Logic Duplicated +`MsgCommand` + `ReplyCommand` still have near-identical send logic. -God class with 60+ methods across 10+ concerns: -- Config orchestration -- Chat settings (global, local, PM) -- Mention settings -- Clear chat state mutation -- Announcement settings -- Join/quit/death message settings - -**Fix:** Split into `ChatConfig`, `PmConfig`, `MentionConfig`, `ClearChatConfig`, `AnnouncementConfig`, `CustomMessagesConfig` - -### 4. OCP: `AdvancedMessageFilter.filterMessage()` - -Hardcoded pipeline of 7 filters. Adding a new filter requires modifying this method. - -**Fix:** Dynamic `List` pipeline with registration - -### 5. ISP: `MessagingService` (fat interface) - -16 methods across 4 concerns: PM + Spy + Ignore + Persistence - -**Fix:** Split into `PrivateMessageService`, `SpyService`, `IgnoreService`, `PersistableService` - -### 6. DIP: Service Locator Anti-Pattern - -`ServiceRegistry.get(Xxx.class)` used everywhere instead of constructor DI. - -**Fix:** Pass required services via constructor parameters - -### 7. DRY: 12 Commands Duplicate Constructor Pattern - -Commands like `MsgCommand`, `ReplyCommand`, `IgnoreCommand`, `MuteCommand`, `BanCommand` etc. don't use `BaseCommand` hierarchy and repeat the same field/constructor/permission pattern. - -**Fix:** Migrate all to `PlayerCommand` / `AdminCommand` - -### 8. DRY: PM Send Logic Duplicated - -`MsgCommand.java:48-59` and `ReplyCommand.java:47-57` have identical PM send code. - -**Fix:** Extract to shared method/service - -### 9. KISS: `MuteCommand.onCommand()` (~100 lines) - -High complexity: argument parsing, flags, fallback logic, permission checks, broadcast logic, voice-mute. - -**Fix:** Decompose into `MuteRequest` parser + `MuteExecutor` service - -### 10. OOP: Leaked Internals - -- `LoChat.getInstance()` — singleton abuse -- `LoChat` exposes 7+ internal components via public getters -- `ChatEventListener` casts to concrete `PlayerServiceImpl` -- `BaseConfig` has `protected` mutable fields +### 5. PM Send Logic Duplicated Between MsgCommand and ReplyCommand --- -## 🔵 MEDIUM PRIORITY +## 🔵 LOW PRIORITY -### 11. Dual Message Configs +### 6. Tests +Only 4 test files for 150+ main files. Very low coverage. -`MessageConfig.java` (root, path-based) and `MessagesConfig.java` (config/, typed getters) overlap. +### 7. Aho-Corasick for Swear Filter +Performance optimization for word filtering. -**Fix:** Unify into single `MessageConfig` - -### 12. Filter Classes Don't Share Interface - -`core/filter/filters/*.java` are standalone. `CooldownFilter`/`MuteFilter` implement `MessageFilter` from api. - -**Fix:** Make all filters implement `MessageFilter` - -### 13. 5+ Services Repeat Load/Save/Persistence - -`NickServiceImpl`, `PlayerServiceImpl`, `ChatServiceImpl`, `PunishmentServiceImpl` all have their own file I/O. - -**Fix:** Shared persistence utility - -### 14. 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. - ---- - -## ⚪ LOW PRIORITY - -### 15. Performance: Aho-Corasick for Swear Filter - -### 16. Discord Rate Limiter - -### 17. Tests: Only 4 test files (150 main files) - -### 18. `.project`, `.classpath`, `.settings/` should be gitignored +### 8. Discord Rate Limiter +Prevent rate limiting when sending many webhook messages. --- diff --git a/bin/main/com/loki/lochat/commands/admin/broadcast/AnnounceCommand.class b/bin/main/com/loki/lochat/commands/admin/broadcast/AnnounceCommand.class index da3260f..912f70f 100644 Binary files a/bin/main/com/loki/lochat/commands/admin/broadcast/AnnounceCommand.class and b/bin/main/com/loki/lochat/commands/admin/broadcast/AnnounceCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/admin/chat/ChatSpyCommand.class b/bin/main/com/loki/lochat/commands/admin/chat/ChatSpyCommand.class index b64736a..049ed8f 100644 Binary files a/bin/main/com/loki/lochat/commands/admin/chat/ChatSpyCommand.class and b/bin/main/com/loki/lochat/commands/admin/chat/ChatSpyCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/admin/chat/ClearChatCommand.class b/bin/main/com/loki/lochat/commands/admin/chat/ClearChatCommand.class index c6624cc..676df06 100644 Binary files a/bin/main/com/loki/lochat/commands/admin/chat/ClearChatCommand.class and b/bin/main/com/loki/lochat/commands/admin/chat/ClearChatCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/admin/system/HubCommand.class b/bin/main/com/loki/lochat/commands/admin/system/HubCommand.class index 2139fbe..fff9ba3 100644 Binary files a/bin/main/com/loki/lochat/commands/admin/system/HubCommand.class and b/bin/main/com/loki/lochat/commands/admin/system/HubCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/chat/LocalChatCommand.class b/bin/main/com/loki/lochat/commands/chat/LocalChatCommand.class index c784e17..be70917 100644 Binary files a/bin/main/com/loki/lochat/commands/chat/LocalChatCommand.class and b/bin/main/com/loki/lochat/commands/chat/LocalChatCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/messaging/IgnoreCommand.class b/bin/main/com/loki/lochat/commands/messaging/IgnoreCommand.class index f662fc2..bdfbbbd 100644 Binary files a/bin/main/com/loki/lochat/commands/messaging/IgnoreCommand.class and b/bin/main/com/loki/lochat/commands/messaging/IgnoreCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/messaging/MsgCommand.class b/bin/main/com/loki/lochat/commands/messaging/MsgCommand.class index ab40ab4..60f57ed 100644 Binary files a/bin/main/com/loki/lochat/commands/messaging/MsgCommand.class and b/bin/main/com/loki/lochat/commands/messaging/MsgCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/messaging/ReplyCommand.class b/bin/main/com/loki/lochat/commands/messaging/ReplyCommand.class index e76663c..d484c1e 100644 Binary files a/bin/main/com/loki/lochat/commands/messaging/ReplyCommand.class and b/bin/main/com/loki/lochat/commands/messaging/ReplyCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/messaging/UnignoreCommand.class b/bin/main/com/loki/lochat/commands/messaging/UnignoreCommand.class index 1dbe248..cfbf30d 100644 Binary files a/bin/main/com/loki/lochat/commands/messaging/UnignoreCommand.class and b/bin/main/com/loki/lochat/commands/messaging/UnignoreCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/moderation/mute/UnmuteCommand.class b/bin/main/com/loki/lochat/commands/moderation/mute/UnmuteCommand.class index 0ba99c5..4d58afc 100644 Binary files a/bin/main/com/loki/lochat/commands/moderation/mute/UnmuteCommand.class and b/bin/main/com/loki/lochat/commands/moderation/mute/UnmuteCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/moderation/warn/SilentWarnCommand.class b/bin/main/com/loki/lochat/commands/moderation/warn/SilentWarnCommand.class index 56ff276..25c9dea 100644 Binary files a/bin/main/com/loki/lochat/commands/moderation/warn/SilentWarnCommand.class and b/bin/main/com/loki/lochat/commands/moderation/warn/SilentWarnCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/moderation/warn/WarnCommand.class b/bin/main/com/loki/lochat/commands/moderation/warn/WarnCommand.class index 4f31393..713e830 100644 Binary files a/bin/main/com/loki/lochat/commands/moderation/warn/WarnCommand.class and b/bin/main/com/loki/lochat/commands/moderation/warn/WarnCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/nick/NickCommand.class b/bin/main/com/loki/lochat/commands/nick/NickCommand.class index 7382e76..996d161 100644 Binary files a/bin/main/com/loki/lochat/commands/nick/NickCommand.class and b/bin/main/com/loki/lochat/commands/nick/NickCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/rp/MeCommand.class b/bin/main/com/loki/lochat/commands/rp/MeCommand.class index 4d65d30..a66079f 100644 Binary files a/bin/main/com/loki/lochat/commands/rp/MeCommand.class and b/bin/main/com/loki/lochat/commands/rp/MeCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/rp/RpUtil.class b/bin/main/com/loki/lochat/commands/rp/RpUtil.class index 3e058b7..b3055dd 100644 Binary files a/bin/main/com/loki/lochat/commands/rp/RpUtil.class and b/bin/main/com/loki/lochat/commands/rp/RpUtil.class differ diff --git a/bin/main/com/loki/lochat/commands/rp/TryCommand.class b/bin/main/com/loki/lochat/commands/rp/TryCommand.class index 4730f50..254a339 100644 Binary files a/bin/main/com/loki/lochat/commands/rp/TryCommand.class and b/bin/main/com/loki/lochat/commands/rp/TryCommand.class differ diff --git a/bin/main/com/loki/lochat/core/factory/ServiceFactory.class b/bin/main/com/loki/lochat/core/factory/ServiceFactory.class index d9c4722..0345592 100644 Binary files a/bin/main/com/loki/lochat/core/factory/ServiceFactory.class and b/bin/main/com/loki/lochat/core/factory/ServiceFactory.class differ diff --git a/bin/main/com/loki/lochat/core/registry/ServiceRegistry.class b/bin/main/com/loki/lochat/core/registry/ServiceRegistry.class index 00f1cb8..9998c3e 100644 Binary files a/bin/main/com/loki/lochat/core/registry/ServiceRegistry.class and b/bin/main/com/loki/lochat/core/registry/ServiceRegistry.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorCopyHandler.class b/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorCopyHandler.class index a981a8e..c0bf267 100644 Binary files a/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorCopyHandler.class and b/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorCopyHandler.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorOffHandler.class b/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorOffHandler.class index 9cc7eb3..c254b34 100644 Binary files a/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorOffHandler.class and b/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorOffHandler.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorOnHandler.class b/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorOnHandler.class index a24cbda..3f4a06f 100644 Binary files a/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorOnHandler.class and b/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorOnHandler.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorResetHandler.class b/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorResetHandler.class index 48b321f..105e876 100644 Binary files a/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorResetHandler.class and b/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorResetHandler.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorSetHandler.class b/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorSetHandler.class index 846a401..221f72b 100644 Binary files a/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorSetHandler.class and b/bin/main/com/loki/lochat/gradient/commands/handlers/color/ColorSetHandler.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixOffHandler.class b/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixOffHandler.class index bee8450..f8f403d 100644 Binary files a/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixOffHandler.class and b/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixOffHandler.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixOnHandler.class b/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixOnHandler.class index 0ee81f1..4860e67 100644 Binary files a/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixOnHandler.class and b/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixOnHandler.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixResetHandler.class b/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixResetHandler.class index 0fab50a..c337767 100644 Binary files a/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixResetHandler.class and b/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixResetHandler.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixSetHandler.class b/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixSetHandler.class index c35e771..c113170 100644 Binary files a/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixSetHandler.class and b/bin/main/com/loki/lochat/gradient/commands/handlers/prefix/PrefixSetHandler.class differ diff --git a/bin/main/com/loki/lochat/gradient/listeners/GradientGUIListener.class b/bin/main/com/loki/lochat/gradient/listeners/GradientGUIListener.class index 37059d4..181303d 100644 Binary files a/bin/main/com/loki/lochat/gradient/listeners/GradientGUIListener.class and b/bin/main/com/loki/lochat/gradient/listeners/GradientGUIListener.class differ diff --git a/bin/main/com/loki/lochat/gradient/listeners/GradientPlayerListener.class b/bin/main/com/loki/lochat/gradient/listeners/GradientPlayerListener.class index 8decb58..6c31908 100644 Binary files a/bin/main/com/loki/lochat/gradient/listeners/GradientPlayerListener.class and b/bin/main/com/loki/lochat/gradient/listeners/GradientPlayerListener.class differ diff --git a/bin/main/com/loki/lochat/integrations/DiscordIntegration.class b/bin/main/com/loki/lochat/integrations/DiscordIntegration.class index 89a0389..09b441b 100644 Binary files a/bin/main/com/loki/lochat/integrations/DiscordIntegration.class and b/bin/main/com/loki/lochat/integrations/DiscordIntegration.class differ diff --git a/bin/main/com/loki/lochat/integrations/DiscordWebhook.class b/bin/main/com/loki/lochat/integrations/DiscordWebhook.class index 1b2c105..6f457fa 100644 Binary files a/bin/main/com/loki/lochat/integrations/DiscordWebhook.class and b/bin/main/com/loki/lochat/integrations/DiscordWebhook.class differ diff --git a/bin/main/com/loki/lochat/integrations/LibertyBansHook.class b/bin/main/com/loki/lochat/integrations/LibertyBansHook.class index 7318f2f..6557a13 100644 Binary files a/bin/main/com/loki/lochat/integrations/LibertyBansHook.class and b/bin/main/com/loki/lochat/integrations/LibertyBansHook.class differ diff --git a/bin/main/com/loki/lochat/integrations/SkinsRestorerHook.class b/bin/main/com/loki/lochat/integrations/SkinsRestorerHook.class index fca0896..6099ae8 100644 Binary files a/bin/main/com/loki/lochat/integrations/SkinsRestorerHook.class and b/bin/main/com/loki/lochat/integrations/SkinsRestorerHook.class differ diff --git a/bin/main/com/loki/lochat/integrations/discord/DiscordConfig.class b/bin/main/com/loki/lochat/integrations/discord/DiscordConfig.class index 55da43a..3139658 100644 Binary files a/bin/main/com/loki/lochat/integrations/discord/DiscordConfig.class and b/bin/main/com/loki/lochat/integrations/discord/DiscordConfig.class differ diff --git a/bin/main/com/loki/lochat/integrations/discord/DiscordMessageService.class b/bin/main/com/loki/lochat/integrations/discord/DiscordMessageService.class index 05d5cad..7e8e740 100644 Binary files a/bin/main/com/loki/lochat/integrations/discord/DiscordMessageService.class and b/bin/main/com/loki/lochat/integrations/discord/DiscordMessageService.class differ diff --git a/bin/main/com/loki/lochat/listener/PlayerEventListener.class b/bin/main/com/loki/lochat/listener/PlayerEventListener.class index 96fbbf5..68f8238 100644 Binary files a/bin/main/com/loki/lochat/listener/PlayerEventListener.class and b/bin/main/com/loki/lochat/listener/PlayerEventListener.class differ diff --git a/bin/main/com/loki/lochat/utils/format/ChatFormatter.class b/bin/main/com/loki/lochat/utils/format/ChatFormatter.class index 338731b..fc6dffb 100644 Binary files a/bin/main/com/loki/lochat/utils/format/ChatFormatter.class and b/bin/main/com/loki/lochat/utils/format/ChatFormatter.class differ diff --git a/bin/main/com/loki/lochat/utils/player/MentionHandler.class b/bin/main/com/loki/lochat/utils/player/MentionHandler.class index d754780..d54d83f 100644 Binary files a/bin/main/com/loki/lochat/utils/player/MentionHandler.class and b/bin/main/com/loki/lochat/utils/player/MentionHandler.class differ diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index a1802f6..d472b45 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -12,6 +12,8 @@ + + @@ -23,6 +25,7 @@ + @@ -103,6 +106,5 @@ - diff --git a/src/main/java/com/loki/lochat/api/service/pm/PrivateMessageService.java b/src/main/java/com/loki/lochat/api/service/pm/PrivateMessageService.java new file mode 100644 index 0000000..5a74744 --- /dev/null +++ b/src/main/java/com/loki/lochat/api/service/pm/PrivateMessageService.java @@ -0,0 +1,20 @@ +package com.loki.lochat.api.service.pm; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.Optional; +import java.util.UUID; + +public interface PrivateMessageService { + + void setLastConversation(UUID player, UUID target); + + Optional getLastConversation(UUID player); + + void removeConversation(UUID player); + + boolean hasConversation(UUID player); + + void sendPrivateMessage(CommandSender sender, Player target, String message); +} diff --git a/src/main/java/com/loki/lochat/commands/messaging/MsgCommand.java b/src/main/java/com/loki/lochat/commands/messaging/MsgCommand.java index 842b7a8..e852dbb 100644 --- a/src/main/java/com/loki/lochat/commands/messaging/MsgCommand.java +++ b/src/main/java/com/loki/lochat/commands/messaging/MsgCommand.java @@ -2,11 +2,10 @@ package com.loki.lochat.commands.messaging; import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MessagingService; +import com.loki.lochat.api.service.pm.PrivateMessageService; import com.loki.lochat.utils.format.ChatFormatter; -import com.loki.lochat.utils.player.PlayerUtil; import org.bukkit.Bukkit; -import org.bukkit.Sound; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -17,10 +16,12 @@ public class MsgCommand implements CommandExecutor { private final LoChat plugin; private final MessagingService messagingService; + private final PrivateMessageService pmService; public MsgCommand(LoChat plugin) { this.plugin = plugin; this.messagingService = plugin.getServiceRegistry().get(MessagingService.class); + this.pmService = plugin.getServiceRegistry().get(PrivateMessageService.class); } @Override @@ -56,24 +57,7 @@ public class MsgCommand implements CommandExecutor { } String message = String.join(" ", java.util.Arrays.copyOfRange(args, 1, args.length)); - sendPm(player, target, message); + pmService.sendPrivateMessage(player, target, message); return true; } - - private void sendPm(Player sender, Player target, String message) { - sender.sendMessage(ChatFormatter.formatPmSentNew(plugin.getMessageConfig().getPmFormatSent(), sender, target, message)); - target.sendMessage(ChatFormatter.formatPmReceivedNew(plugin.getMessageConfig().getPmFormatReceived(), sender, target, message)); - if (plugin.getConfigManager().isPmSoundEnabled()) { - Sound sound = PlayerUtil.parseSound(plugin.getConfigManager().getPmSoundType(), null); - if (sound != null) { - target.playSound(target.getLocation(), sound, 1.0f, 1.0f); - } - } - messagingService.broadcastPM(sender, target, message); - messagingService.setLastConversation(sender.getUniqueId(), target.getUniqueId()); - messagingService.setLastConversation(target.getUniqueId(), sender.getUniqueId()); - if (plugin.getConfigManager().isPmLogEnabled()) { - plugin.getLogger().info("[PM] " + sender.getName() + " -> " + target.getName() + ": " + message); - } - } } diff --git a/src/main/java/com/loki/lochat/commands/messaging/ReplyCommand.java b/src/main/java/com/loki/lochat/commands/messaging/ReplyCommand.java index 6155ecd..86c50f9 100644 --- a/src/main/java/com/loki/lochat/commands/messaging/ReplyCommand.java +++ b/src/main/java/com/loki/lochat/commands/messaging/ReplyCommand.java @@ -2,11 +2,10 @@ package com.loki.lochat.commands.messaging; import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MessagingService; +import com.loki.lochat.api.service.pm.PrivateMessageService; import com.loki.lochat.utils.format.ChatFormatter; -import com.loki.lochat.utils.player.PlayerUtil; import org.bukkit.Bukkit; -import org.bukkit.Sound; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -20,10 +19,12 @@ public class ReplyCommand implements CommandExecutor { private final LoChat plugin; private final MessagingService messagingService; + private final PrivateMessageService pmService; public ReplyCommand(LoChat plugin) { this.plugin = plugin; this.messagingService = plugin.getServiceRegistry().get(MessagingService.class); + this.pmService = plugin.getServiceRegistry().get(PrivateMessageService.class); } @Override @@ -57,19 +58,7 @@ public class ReplyCommand implements CommandExecutor { } String message = String.join(" ", args); - player.sendMessage(ChatFormatter.formatPmSentNew(plugin.getMessageConfig().getPmFormatSent(), player, target, message)); - target.sendMessage(ChatFormatter.formatPmReceivedNew(plugin.getMessageConfig().getPmFormatReceived(), player, target, message)); - if (plugin.getConfigManager().isPmSoundEnabled()) { - Sound sound = PlayerUtil.parseSound(plugin.getConfigManager().getPmSoundType(), null); - if (sound != null) { - target.playSound(target.getLocation(), sound, 1.0f, 1.0f); - } - } - messagingService.broadcastPM(player, target, message); - messagingService.setLastConversation(target.getUniqueId(), player.getUniqueId()); - if (plugin.getConfigManager().isPmLogEnabled()) { - plugin.getLogger().info("[PM] " + player.getName() + " -> " + target.getName() + ": " + message); - } + pmService.sendPrivateMessage(player, target, message); return true; } } diff --git a/src/main/java/com/loki/lochat/core/factory/ServiceFactory.java b/src/main/java/com/loki/lochat/core/factory/ServiceFactory.java index 6260991..40dad7d 100644 --- a/src/main/java/com/loki/lochat/core/factory/ServiceFactory.java +++ b/src/main/java/com/loki/lochat/core/factory/ServiceFactory.java @@ -5,6 +5,7 @@ import com.loki.lochat.api.service.MuteService; import com.loki.lochat.config.MessageConfig; import com.loki.lochat.core.service.MessagingServiceImpl; import com.loki.lochat.core.service.MuteServiceImpl; +import com.loki.lochat.core.service.messaging.PrivateMessageServiceImpl; import com.loki.lochat.core.service.mute.MuteDataStorage; import com.loki.lochat.core.service.mute.MuteHistoryManager; import com.loki.lochat.core.service.mute.strategies.ChatMuteStrategy; @@ -50,7 +51,8 @@ public class ServiceFactory { /** * Создать MessagingService со всеми зависимостями */ - public static MessagingService createMessagingService(JavaPlugin plugin, MessageConfig messageConfig) { - return new MessagingServiceImpl(plugin, messageConfig); + public static MessagingService createMessagingService( + JavaPlugin plugin, MessageConfig messageConfig, PrivateMessageServiceImpl pmService) { + return new MessagingServiceImpl(plugin, messageConfig, pmService); } } 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 e73f906..6cd5fc7 100644 --- a/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java +++ b/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java @@ -1,5 +1,6 @@ package com.loki.lochat.core.registry; +import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChatService; import com.loki.lochat.api.service.MentionService; import com.loki.lochat.api.service.MessageService; @@ -8,6 +9,7 @@ import com.loki.lochat.api.service.MuteService; import com.loki.lochat.api.service.NickService; import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.api.service.PunishmentService; +import com.loki.lochat.api.service.pm.PrivateMessageService; import com.loki.lochat.config.ConfigManager; import com.loki.lochat.config.MessageConfig; import com.loki.lochat.core.factory.ServiceFactory; @@ -17,6 +19,7 @@ import com.loki.lochat.core.service.MessageServiceImpl; import com.loki.lochat.core.service.NickServiceImpl; import com.loki.lochat.core.service.PlayerServiceImpl; import com.loki.lochat.core.service.PunishmentServiceImpl; +import com.loki.lochat.core.service.messaging.PrivateMessageServiceImpl; import org.bukkit.plugin.java.JavaPlugin; @@ -48,7 +51,10 @@ public class ServiceRegistry { register(MessageService.class, new MessageServiceImpl(plugin, this)); // ✅ NEW: Объединённый MessagingService (PM + Spy + Ignore) через фабрику - MessagingService messagingService = ServiceFactory.createMessagingService(plugin, messageConfig); + PrivateMessageServiceImpl pmService = new PrivateMessageServiceImpl((LoChat) plugin); + MessagingService messagingService = ServiceFactory.createMessagingService(plugin, messageConfig, pmService); + pmService.init(messagingService); + register(PrivateMessageService.class, pmService); register(MessagingService.class, messagingService); register(MentionService.class, new MentionServiceImpl(configManager)); 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 0380a89..1cf050a 100644 --- a/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java @@ -4,18 +4,18 @@ import com.loki.lochat.api.service.ChatService; import com.loki.lochat.core.registry.ServiceRegistry; import com.loki.lochat.renderer.EnhancedChatRenderer; import com.loki.lochat.utils.format.ChatFormatter; +import com.loki.lochat.utils.persistence.FilePersistence; import com.loki.lochat.utils.platform.FoliaUtil; import com.loki.lochat.utils.player.DistanceUtil; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; -import java.io.File; -import java.io.IOException; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -28,11 +28,9 @@ public class ChatServiceImpl implements ChatService { private final JavaPlugin plugin; private final Set globalChatDisabled = ConcurrentHashMap.newKeySet(); - private final File playersFile; public ChatServiceImpl(JavaPlugin plugin, ServiceRegistry registry) { this.plugin = plugin; - this.playersFile = new File(plugin.getDataFolder(), "data/players.yml"); loadDisabled(); } @@ -99,10 +97,7 @@ public class ChatServiceImpl implements ChatService { // ────────────────────────────────────────────────────────────────────────── private void loadDisabled() { - if (!playersFile.exists()) { - return; - } - YamlConfiguration cfg = YamlConfiguration.loadConfiguration(playersFile); + FileConfiguration cfg = FilePersistence.loadYaml(plugin, "data/players.yml"); for (String uuidStr : cfg.getStringList("global-chat-disabled")) { try { globalChatDisabled.add(UUID.fromString(uuidStr)); @@ -112,25 +107,10 @@ public class ChatServiceImpl implements ChatService { } private void saveDisabled() { - try { - ensureDir(); - YamlConfiguration cfg = playersFile.exists() - ? YamlConfiguration.loadConfiguration(playersFile) - : new YamlConfiguration(); - - cfg.set("global-chat-disabled", - globalChatDisabled.stream().map(UUID::toString).toList()); - cfg.save(playersFile); - } catch (IOException e) { - plugin.getLogger().warning("[LoChat] Failed to save players.yml: " + e.getMessage()); - } - } - - private void ensureDir() { - File dir = playersFile.getParentFile(); - if (!dir.exists()) { - dir.mkdirs(); - } + YamlConfiguration cfg = new YamlConfiguration(); + cfg.set("global-chat-disabled", + globalChatDisabled.stream().map(UUID::toString).toList()); + FilePersistence.saveYaml(plugin, "data/players.yml", cfg); } private Component toComponent(Object message) { diff --git a/src/main/java/com/loki/lochat/core/service/MessagingServiceImpl.java b/src/main/java/com/loki/lochat/core/service/MessagingServiceImpl.java index ddd58cd..ead1c0f 100644 --- a/src/main/java/com/loki/lochat/core/service/MessagingServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/MessagingServiceImpl.java @@ -3,7 +3,7 @@ package com.loki.lochat.core.service; import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.config.MessageConfig; import com.loki.lochat.core.service.messaging.IgnoreService; -import com.loki.lochat.core.service.messaging.PrivateMessageService; +import com.loki.lochat.core.service.messaging.PrivateMessageServiceImpl; import com.loki.lochat.core.service.messaging.SpyService; import net.kyori.adventure.text.Component; @@ -21,12 +21,12 @@ import java.util.UUID; */ public class MessagingServiceImpl implements MessagingService { - private final PrivateMessageService pmService; + private final PrivateMessageServiceImpl pmService; private final SpyService spyService; private final IgnoreService ignoreService; - public MessagingServiceImpl(JavaPlugin plugin, MessageConfig messageConfig) { - this.pmService = new PrivateMessageService(); + public MessagingServiceImpl(JavaPlugin plugin, MessageConfig messageConfig, PrivateMessageServiceImpl pmService) { + this.pmService = pmService; this.spyService = new SpyService(messageConfig); this.ignoreService = new IgnoreService(plugin); this.ignoreService.init(); diff --git a/src/main/java/com/loki/lochat/core/service/NickServiceImpl.java b/src/main/java/com/loki/lochat/core/service/NickServiceImpl.java index 76f294f..c167bf0 100644 --- a/src/main/java/com/loki/lochat/core/service/NickServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/NickServiceImpl.java @@ -3,18 +3,16 @@ package com.loki.lochat.core.service; import com.loki.lochat.api.service.NickService; import com.loki.lochat.config.RatConfig; import com.loki.lochat.utils.format.ChatFormatter; +import com.loki.lochat.utils.persistence.FilePersistence; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.Bukkit; import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; -import java.io.File; -import java.io.IOException; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -26,26 +24,15 @@ import java.util.concurrent.ConcurrentHashMap; public class NickServiceImpl implements NickService { private final JavaPlugin plugin; private final Map nicknames = new ConcurrentHashMap<>(); - private final File dataFile; private FileConfiguration data; public NickServiceImpl(JavaPlugin plugin) { this.plugin = plugin; - this.dataFile = new File(plugin.getDataFolder(), "nicknames.yml"); load(); } private void load() { - if (!dataFile.exists()) { - try { - dataFile.getParentFile().mkdirs(); - dataFile.createNewFile(); - } catch (IOException e) { - plugin.getLogger().warning("Не удалось создать nicknames.yml: " + e.getMessage()); - } - } - - data = YamlConfiguration.loadConfiguration(dataFile); + data = FilePersistence.loadYaml(plugin, "nicknames.yml"); // Загружаем ники if (data.contains("nicknames")) { @@ -67,11 +54,7 @@ public class NickServiceImpl implements NickService { data.set("nicknames." + entry.getKey().toString(), entry.getValue()); } - try { - data.save(dataFile); - } catch (IOException e) { - plugin.getLogger().warning("Не удалось сохранить nicknames.yml: " + e.getMessage()); - } + FilePersistence.saveYaml(plugin, "nicknames.yml", data); } @Override 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 bbe0991..2d031c1 100644 --- a/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java @@ -1,13 +1,12 @@ package com.loki.lochat.core.service; import com.loki.lochat.api.service.PlayerService; +import com.loki.lochat.utils.persistence.FilePersistence; import com.loki.lochat.utils.platform.FoliaUtil; -import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.plugin.java.JavaPlugin; -import java.io.File; -import java.io.IOException; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -32,12 +31,10 @@ public class PlayerServiceImpl implements PlayerService { private final AtomicLong pmCount = new AtomicLong(0); private final AtomicLong totalMessages = new AtomicLong(0); - private final File statsFile; - private YamlConfiguration stats; - + private FileConfiguration stats; + public PlayerServiceImpl(JavaPlugin plugin) { this.plugin = plugin; - this.statsFile = new File(plugin.getDataFolder(), "data/statistics.yml"); load(); } @@ -116,51 +113,33 @@ public class PlayerServiceImpl implements PlayerService { @Override public void saveAll() { - try { - ensureDir(); - // Обновляем глобальные счётчики - 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()); - stats.save(statsFile); - } catch (IOException e) { - plugin.getLogger().warning("[LoChat] Failed to save statistics: " + e.getMessage()); - } + 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) { - try { - 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()); - stats.save(statsFile); - playerMessages.remove(uuid); - } catch (IOException e) { - plugin.getLogger().warning("[LoChat] Failed to persist player stats: " + e.getMessage()); - } + 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); } private void load() { - ensureDir(); - if (!statsFile.exists()) { + if (!FilePersistence.getFile(plugin, "data/statistics.yml").exists()) { plugin.saveResource("data/statistics.yml", false); } - stats = YamlConfiguration.loadConfiguration(statsFile); - } - - private void ensureDir() { - File dir = statsFile.getParentFile(); - if (!dir.exists()) { - dir.mkdirs(); - } + stats = FilePersistence.loadYaml(plugin, "data/statistics.yml"); } } 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 168fae1..35e64aa 100644 --- a/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java @@ -2,28 +2,19 @@ package com.loki.lochat.core.service; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; import com.loki.lochat.api.service.PunishmentService; import com.loki.lochat.config.MessagesConfig; import com.loki.lochat.data.model.BanRecord; import com.loki.lochat.data.model.WarnEntry; import com.loki.lochat.utils.format.ChatFormatter; import com.loki.lochat.utils.format.TimeFormatter; +import com.loki.lochat.utils.persistence.FilePersistence; import com.loki.lochat.utils.platform.FoliaUtil; import net.kyori.adventure.text.Component; import org.bukkit.plugin.java.JavaPlugin; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.Writer; -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -39,7 +30,6 @@ public class PunishmentServiceImpl implements PunishmentService { private final JavaPlugin plugin; private final MessagesConfig messages; - private final File dataFile; private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); private final Map> warns = new ConcurrentHashMap<>(); @@ -48,7 +38,6 @@ public class PunishmentServiceImpl implements PunishmentService { public PunishmentServiceImpl(JavaPlugin plugin, MessagesConfig messagesConfig) { this.plugin = plugin; this.messages = messagesConfig; - this.dataFile = new File(plugin.getDataFolder(), "punishments.json"); load(); } @@ -141,26 +130,17 @@ public class PunishmentServiceImpl implements PunishmentService { @Override public void save() { - try { - if (!dataFile.getParentFile().exists() && !dataFile.getParentFile().mkdirs()) { - plugin.getLogger().warning("Could not create data folder for punishments"); + PunishmentSnapshot snap = new PunishmentSnapshot(); + for (Map.Entry> e : warns.entrySet()) { + List list = e.getValue(); + synchronized (list) { + snap.getWarns().put(e.getKey().toString(), new ArrayList<>(list)); } - PunishmentSnapshot snap = new PunishmentSnapshot(); - for (Map.Entry> e : warns.entrySet()) { - List list = e.getValue(); - synchronized (list) { - snap.getWarns().put(e.getKey().toString(), new ArrayList<>(list)); - } - } - for (Map.Entry e : bans.entrySet()) { - snap.getBans().put(e.getKey().toString(), e.getValue()); - } - try (Writer w = new OutputStreamWriter(new FileOutputStream(dataFile), java.nio.charset.StandardCharsets.UTF_8)) { - gson.toJson(snap, w); - } - } catch (IOException e) { - plugin.getLogger().warning("Failed to save punishments: " + e.getMessage()); } + for (Map.Entry e : bans.entrySet()) { + snap.getBans().put(e.getKey().toString(), e.getValue()); + } + FilePersistence.saveJson(plugin, "punishments.json", snap, gson); } @Override @@ -175,41 +155,33 @@ public class PunishmentServiceImpl implements PunishmentService { } private void load() { - if (!dataFile.exists()) { + PunishmentSnapshot snap = FilePersistence.loadJson(plugin, "punishments.json", PunishmentSnapshot.class, gson); + if (snap == null) { return; } - try (Reader r = new InputStreamReader(new FileInputStream(dataFile), java.nio.charset.StandardCharsets.UTF_8)) { - Type type = new TypeToken() { }.getType(); - PunishmentSnapshot snap = gson.fromJson(r, type); - if (snap == null) { - return; - } - if (snap.getWarns() != null) { - for (Map.Entry> e : snap.getWarns().entrySet()) { - try { - warns.put(UUID.fromString(e.getKey()), Collections.synchronizedList(new ArrayList<>(e.getValue()))); - } catch (IllegalArgumentException ignored) { - } + if (snap.getWarns() != null) { + for (Map.Entry> e : snap.getWarns().entrySet()) { + try { + warns.put(UUID.fromString(e.getKey()), Collections.synchronizedList(new ArrayList<>(e.getValue()))); + } catch (IllegalArgumentException ignored) { } } - if (snap.getBans() != null) { - for (Map.Entry e : snap.getBans().entrySet()) { - try { - UUID id = UUID.fromString(e.getKey()); - BanRecord br = e.getValue(); - if (br != null) { - br.uuid = id; - if (!br.isPermanent() && br.isExpired()) { - continue; - } - bans.put(id, br); + } + if (snap.getBans() != null) { + for (Map.Entry e : snap.getBans().entrySet()) { + try { + UUID id = UUID.fromString(e.getKey()); + BanRecord br = e.getValue(); + if (br != null) { + br.uuid = id; + if (!br.isPermanent() && br.isExpired()) { + continue; } - } catch (IllegalArgumentException ignored) { + bans.put(id, br); } + } catch (IllegalArgumentException ignored) { } } - } catch (IOException e) { - plugin.getLogger().warning("Failed to load punishments: " + e.getMessage()); } } diff --git a/src/main/java/com/loki/lochat/core/service/messaging/IgnoreService.java b/src/main/java/com/loki/lochat/core/service/messaging/IgnoreService.java index bff93f4..fedf01c 100644 --- a/src/main/java/com/loki/lochat/core/service/messaging/IgnoreService.java +++ b/src/main/java/com/loki/lochat/core/service/messaging/IgnoreService.java @@ -1,12 +1,12 @@ package com.loki.lochat.core.service.messaging; +import com.loki.lochat.utils.persistence.FilePersistence; import com.loki.lochat.utils.platform.FoliaUtil; +import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.java.JavaPlugin; -import java.io.File; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -23,11 +23,9 @@ public class IgnoreService { private final JavaPlugin plugin; private final Map> ignoreMap = new ConcurrentHashMap<>(); - private final File ignoreFile; public IgnoreService(JavaPlugin plugin) { this.plugin = plugin; - this.ignoreFile = new File(plugin.getDataFolder(), "ignores.yml"); } public void init() { @@ -75,11 +73,7 @@ public class IgnoreService { } public void load() { - if (!ignoreFile.exists()) { - return; - } - - YamlConfiguration config = YamlConfiguration.loadConfiguration(ignoreFile); + FileConfiguration config = FilePersistence.loadYaml(plugin, "ignores.yml"); for (String key : config.getKeys(false)) { try { UUID uuid = UUID.fromString(key); @@ -114,11 +108,7 @@ public class IgnoreService { config.set(entry.getKey().toString(), ignoredList); } - try { - config.save(ignoreFile); - } catch (IOException e) { - plugin.getLogger().severe("Не удалось сохранить игноры: " + e.getMessage()); - } + FilePersistence.saveYaml(plugin, "ignores.yml", config); } private void saveAsync() { diff --git a/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageService.java b/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageService.java deleted file mode 100644 index 21281d9..0000000 --- a/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.loki.lochat.core.service.messaging; - -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Сервис личных сообщений (PM) - * Отслеживает последние диалоги между игроками - */ -public class PrivateMessageService { - - private final Map lastConversation = new ConcurrentHashMap<>(); - - public void setLastConversation(UUID player, UUID target) { - lastConversation.put(player, target); - } - - public Optional getLastConversation(UUID player) { - return Optional.ofNullable(lastConversation.get(player)); - } - - public void removeConversation(UUID player) { - lastConversation.remove(player); - } - - public boolean hasConversation(UUID player) { - return lastConversation.containsKey(player); - } -} diff --git a/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageServiceImpl.java b/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageServiceImpl.java new file mode 100644 index 0000000..f4ecf4e --- /dev/null +++ b/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageServiceImpl.java @@ -0,0 +1,76 @@ +package com.loki.lochat.core.service.messaging; + +import com.loki.lochat.LoChat; +import com.loki.lochat.api.service.MessagingService; +import com.loki.lochat.api.service.pm.PrivateMessageService; +import com.loki.lochat.utils.format.ChatFormatter; +import com.loki.lochat.utils.player.PlayerUtil; + +import org.bukkit.Sound; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Сервис личных сообщений (PM) + * Отслеживает последние диалоги между игроками + */ +public class PrivateMessageServiceImpl implements PrivateMessageService { + + private final Map lastConversation = new ConcurrentHashMap<>(); + private final LoChat plugin; + private MessagingService messagingService; + + public PrivateMessageServiceImpl(LoChat plugin) { + this.plugin = plugin; + } + + public void init(MessagingService messagingService) { + this.messagingService = messagingService; + } + + @Override + public void setLastConversation(UUID player, UUID target) { + lastConversation.put(player, target); + } + + @Override + public Optional getLastConversation(UUID player) { + return Optional.ofNullable(lastConversation.get(player)); + } + + @Override + public void removeConversation(UUID player) { + lastConversation.remove(player); + } + + @Override + public boolean hasConversation(UUID player) { + return lastConversation.containsKey(player); + } + + @Override + public void sendPrivateMessage(CommandSender sender, Player target, String message) { + Player playerSender = (Player) sender; + sender.sendMessage(ChatFormatter.formatPmSentNew( + plugin.getMessageConfig().getPmFormatSent(), playerSender, target, message)); + target.sendMessage(ChatFormatter.formatPmReceivedNew( + plugin.getMessageConfig().getPmFormatReceived(), playerSender, target, message)); + if (plugin.getConfigManager().isPmSoundEnabled()) { + Sound sound = PlayerUtil.parseSound(plugin.getConfigManager().getPmSoundType(), null); + if (sound != null) { + target.playSound(target.getLocation(), sound, 1.0f, 1.0f); + } + } + messagingService.broadcastPM(playerSender, target, message); + messagingService.setLastConversation(playerSender.getUniqueId(), target.getUniqueId()); + messagingService.setLastConversation(target.getUniqueId(), playerSender.getUniqueId()); + if (plugin.getConfigManager().isPmLogEnabled()) { + plugin.getLogger().info("[PM] " + playerSender.getName() + " -> " + target.getName() + ": " + message); + } + } +} diff --git a/src/main/java/com/loki/lochat/integrations/DiscordWebhook.java b/src/main/java/com/loki/lochat/integrations/DiscordWebhook.java index 3e160fd..cec75d1 100644 --- a/src/main/java/com/loki/lochat/integrations/DiscordWebhook.java +++ b/src/main/java/com/loki/lochat/integrations/DiscordWebhook.java @@ -2,6 +2,7 @@ package com.loki.lochat.integrations; import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.loki.lochat.integrations.discord.DiscordRateLimiter; import org.bukkit.plugin.java.JavaPlugin; @@ -29,6 +30,7 @@ public class DiscordWebhook { private final int timeout; private final int retryAttempts; private final long retryDelay; + private final DiscordRateLimiter rateLimiter; // Выделенный пул — не засоряем ForkJoinPool.commonPool() private final ExecutorService executor = Executors.newSingleThreadExecutor( @@ -39,8 +41,10 @@ public class DiscordWebhook { } ); + @SuppressWarnings("checkstyle:ParameterNumber") public DiscordWebhook(JavaPlugin plugin, String webhookUrl, String username, String avatarUrl, - int timeout, int retryAttempts, long retryDelay) { + int timeout, int retryAttempts, long retryDelay, + double maxRequestsPerSecond, int rateLimitBurst) { this.plugin = plugin; this.webhookUrl = webhookUrl != null ? webhookUrl.trim() : null; this.username = username; @@ -48,6 +52,7 @@ public class DiscordWebhook { this.timeout = timeout * 1000; this.retryAttempts = retryAttempts; this.retryDelay = retryDelay; + this.rateLimiter = new DiscordRateLimiter(maxRequestsPerSecond, rateLimitBurst); } // ── Публичные методы отправки ───────────────────────────────────────────── @@ -114,6 +119,7 @@ public class DiscordWebhook { private CompletableFuture sendWebhook(JsonObject json) { return CompletableFuture.supplyAsync(() -> { for (int attempt = 1; attempt <= retryAttempts; attempt++) { + rateLimiter.waitIfNeeded(); try { if (sendWebhookSync(json.toString())) { return 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 8594f59..ef0428e 100644 --- a/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java +++ b/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java @@ -70,6 +70,14 @@ public class DiscordConfig { public boolean isAsync() { return config.getBoolean("performance.async", true); } + + public double getMaxRequestsPerSecond() { + return config.getDouble("performance.max-requests-per-second", 5.0); + } + + public int getRateLimitBurst() { + return config.getInt("performance.rate-limit-burst", 10); + } public boolean isChatEnabled() { return config.getBoolean("chat.enabled", true); diff --git a/src/main/java/com/loki/lochat/integrations/discord/DiscordMessageService.java b/src/main/java/com/loki/lochat/integrations/discord/DiscordMessageService.java index 270ceb6..16287e8 100644 --- a/src/main/java/com/loki/lochat/integrations/discord/DiscordMessageService.java +++ b/src/main/java/com/loki/lochat/integrations/discord/DiscordMessageService.java @@ -29,7 +29,9 @@ public class DiscordMessageService { getAvatarUrl(), config.getTimeout(), config.getRetryAttempts(), - config.getRetryDelay() + config.getRetryDelay(), + config.getMaxRequestsPerSecond(), + config.getRateLimitBurst() ); } diff --git a/src/main/java/com/loki/lochat/integrations/discord/DiscordRateLimiter.java b/src/main/java/com/loki/lochat/integrations/discord/DiscordRateLimiter.java new file mode 100644 index 0000000..3c5c88f --- /dev/null +++ b/src/main/java/com/loki/lochat/integrations/discord/DiscordRateLimiter.java @@ -0,0 +1,53 @@ +package com.loki.lochat.integrations.discord; + +public class DiscordRateLimiter { + + private final double refillRate; + private final double capacity; + private double tokens; + private long lastRefillTime; + + public DiscordRateLimiter(double maxRequestsPerSecond, double burst) { + this.refillRate = maxRequestsPerSecond; + this.capacity = burst; + this.tokens = burst; + this.lastRefillTime = System.nanoTime(); + } + + public synchronized boolean tryAcquire() { + refill(); + if (tokens >= 1.0) { + tokens -= 1.0; + return true; + } + return false; + } + + public void waitIfNeeded() { + long sleepMs; + while (true) { + synchronized (this) { + refill(); + if (tokens >= 1.0) { + tokens -= 1.0; + return; + } + double waitTimeMs = ((1.0 - tokens) / refillRate) * 1000.0; + sleepMs = Math.min((long) Math.ceil(waitTimeMs), 100); + } + try { + Thread.sleep(Math.max(sleepMs, 1)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + + private void refill() { + long now = System.nanoTime(); + double elapsed = (now - lastRefillTime) / 1_000_000_000.0; + tokens = Math.min(capacity, tokens + elapsed * refillRate); + lastRefillTime = now; + } +} diff --git a/src/main/java/com/loki/lochat/utils/persistence/FilePersistence.java b/src/main/java/com/loki/lochat/utils/persistence/FilePersistence.java new file mode 100644 index 0000000..35857f1 --- /dev/null +++ b/src/main/java/com/loki/lochat/utils/persistence/FilePersistence.java @@ -0,0 +1,70 @@ +package com.loki.lochat.utils.persistence; + +import com.google.gson.Gson; + +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.StandardCharsets; + +public final class FilePersistence { + + private FilePersistence() {} + + public static File getFile(JavaPlugin plugin, String name) { + return new File(plugin.getDataFolder(), name); + } + + public static void ensureDataFolder(JavaPlugin plugin) { + plugin.getDataFolder().mkdirs(); + } + + public static FileConfiguration loadYaml(JavaPlugin plugin, String name) { + File file = getFile(plugin, name); + return file.exists() ? YamlConfiguration.loadConfiguration(file) : new YamlConfiguration(); + } + + public static void saveYaml(JavaPlugin plugin, String name, FileConfiguration config) { + try { + File file = getFile(plugin, name); + file.getParentFile().mkdirs(); + config.save(file); + } catch (IOException e) { + plugin.getLogger().warning("Failed to save " + name + ": " + e.getMessage()); + } + } + + public static T loadJson(JavaPlugin plugin, String name, Class clazz, Gson gson) { + File file = getFile(plugin, name); + if (!file.exists()) { + return null; + } + try (Reader reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) { + return gson.fromJson(reader, clazz); + } catch (IOException e) { + plugin.getLogger().warning("Failed to load " + name + ": " + e.getMessage()); + return null; + } + } + + public static void saveJson(JavaPlugin plugin, String name, Object data, Gson gson) { + try { + File file = getFile(plugin, name); + file.getParentFile().mkdirs(); + try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) { + gson.toJson(data, writer); + } + } catch (IOException e) { + plugin.getLogger().warning("Failed to save " + name + ": " + e.getMessage()); + } + } +} diff --git a/src/main/resources/config/discord.yml b/src/main/resources/config/discord.yml index d8c849c..17b7972 100644 --- a/src/main/resources/config/discord.yml +++ b/src/main/resources/config/discord.yml @@ -71,3 +71,5 @@ performance: timeout: 10 retry-attempts: 3 retry-delay: 1000 + max-requests-per-second: 5 + rate-limit-burst: 10 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index b6c8071..38b6a7e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -281,6 +281,9 @@ permissions: lochat.mute.see-silent: description: Can see silent mute notifications default: op + lochat.mute.dur.5m: + description: Can mute for 5 minutes + default: false lochat.mute.dur.10m: description: Can mute for 10 minutes default: false @@ -290,6 +293,9 @@ permissions: lochat.mute.dur.1h: description: Can mute for 1 hour default: false + lochat.mute.dur.3h: + description: Can mute for 3 hours + default: false lochat.mute.dur.6h: description: Can mute for 6 hours default: false @@ -315,6 +321,46 @@ permissions: description: Can mute permanently default: op + lochat.mute.reason.spam: + description: Can use spam mute reason + default: false + lochat.mute.reason.caps: + description: Can use caps mute reason + default: false + lochat.mute.reason.swear: + description: Can use swear mute reason + default: false + lochat.mute.reason.flood: + description: Can use flood mute reason + default: false + lochat.mute.reason.advertising: + description: Can use advertising mute reason + default: false + lochat.mute.reason.insult: + description: Can use insult mute reason + default: false + lochat.mute.reason.toxicity: + description: Can use toxicity mute reason + default: false + lochat.mute.reason.politics: + description: Can use politics mute reason + default: false + lochat.mute.reason.religion: + description: Can use religion mute reason + default: false + lochat.mute.reason.begging: + description: Can use begging mute reason + default: false + lochat.mute.notify: + description: Receive mute notifications + default: op + lochat.mute.ip: + description: Can mute by IP address + default: false + lochat.appeal: + description: Can use /appeal command + default: true + lochat.warn: description: Issue a visible warning default: op @@ -348,6 +394,9 @@ permissions: lochat.heademoji.use: description: Can use head emojis in chat default: true + chat.emoji.use: + description: Can use emojis in chat + default: true lochat.nick: description: Can use custom nicknames default: true