From 42ebac890ce1038973d545211e41e27521cdd693 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 5 Jul 2026 15:17:15 +0200 Subject: [PATCH] feat: persistence util, PM DRY, Discord rate limiter, missing permissions in plugin.yml --- TODO.md | 176 ++++-------------- .../admin/broadcast/AnnounceCommand.class | Bin 1145 -> 1145 bytes .../commands/admin/chat/ChatSpyCommand.class | Bin 1837 -> 1837 bytes .../admin/chat/ClearChatCommand.class | Bin 1057 -> 1057 bytes .../commands/admin/system/HubCommand.class | Bin 1037 -> 1037 bytes .../commands/chat/LocalChatCommand.class | Bin 1852 -> 1852 bytes .../commands/messaging/IgnoreCommand.class | Bin 2670 -> 2670 bytes .../commands/messaging/MsgCommand.class | Bin 3506 -> 3176 bytes .../commands/messaging/ReplyCommand.class | Bin 3041 -> 2996 bytes .../commands/messaging/UnignoreCommand.class | Bin 2490 -> 2490 bytes .../moderation/mute/UnmuteCommand.class | Bin 2785 -> 2785 bytes .../moderation/warn/SilentWarnCommand.class | Bin 2434 -> 2434 bytes .../moderation/warn/WarnCommand.class | Bin 2696 -> 2696 bytes .../lochat/commands/nick/NickCommand.class | Bin 5483 -> 5483 bytes .../loki/lochat/commands/rp/MeCommand.class | Bin 1713 -> 1713 bytes .../com/loki/lochat/commands/rp/RpUtil.class | Bin 2474 -> 2474 bytes .../loki/lochat/commands/rp/TryCommand.class | Bin 1754 -> 1754 bytes .../lochat/core/factory/ServiceFactory.class | Bin 1064 -> 1130 bytes .../core/registry/ServiceRegistry.class | Bin 4027 -> 4235 bytes .../handlers/color/ColorCopyHandler.class | Bin 5431 -> 5431 bytes .../handlers/color/ColorOffHandler.class | Bin 3372 -> 3372 bytes .../handlers/color/ColorOnHandler.class | Bin 3479 -> 3479 bytes .../handlers/color/ColorResetHandler.class | Bin 3581 -> 3581 bytes .../handlers/color/ColorSetHandler.class | Bin 5973 -> 5973 bytes .../handlers/prefix/PrefixOffHandler.class | Bin 3390 -> 3390 bytes .../handlers/prefix/PrefixOnHandler.class | Bin 3498 -> 3498 bytes .../handlers/prefix/PrefixResetHandler.class | Bin 3574 -> 3574 bytes .../handlers/prefix/PrefixSetHandler.class | Bin 6512 -> 6512 bytes .../listeners/GradientGUIListener.class | Bin 7162 -> 7162 bytes .../listeners/GradientPlayerListener.class | Bin 3496 -> 3496 bytes .../integrations/DiscordIntegration.class | Bin 3053 -> 3053 bytes .../lochat/integrations/DiscordWebhook.class | Bin 8791 -> 9089 bytes .../lochat/integrations/LibertyBansHook.class | Bin 1256 -> 1256 bytes .../integrations/SkinsRestorerHook.class | Bin 3270 -> 3270 bytes .../integrations/discord/DiscordConfig.class | Bin 5149 -> 5469 bytes .../discord/DiscordMessageService.class | Bin 6024 -> 6114 bytes .../lochat/listener/PlayerEventListener.class | Bin 3411 -> 3411 bytes .../lochat/utils/format/ChatFormatter.class | Bin 7820 -> 7820 bytes .../lochat/utils/player/MentionHandler.class | Bin 6800 -> 6800 bytes config/checkstyle/checkstyle.xml | 4 +- .../api/service/pm/PrivateMessageService.java | 20 ++ .../lochat/commands/messaging/MsgCommand.java | 24 +-- .../commands/messaging/ReplyCommand.java | 19 +- .../lochat/core/factory/ServiceFactory.java | 6 +- .../lochat/core/registry/ServiceRegistry.java | 8 +- .../lochat/core/service/ChatServiceImpl.java | 34 +--- .../core/service/MessagingServiceImpl.java | 8 +- .../lochat/core/service/NickServiceImpl.java | 23 +-- .../core/service/PlayerServiceImpl.java | 65 +++---- .../core/service/PunishmentServiceImpl.java | 86 +++------ .../core/service/messaging/IgnoreService.java | 18 +- .../messaging/PrivateMessageService.java | 31 --- .../messaging/PrivateMessageServiceImpl.java | 76 ++++++++ .../lochat/integrations/DiscordWebhook.java | 8 +- .../integrations/discord/DiscordConfig.java | 8 + .../discord/DiscordMessageService.java | 4 +- .../discord/DiscordRateLimiter.java | 53 ++++++ .../utils/persistence/FilePersistence.java | 70 +++++++ src/main/resources/config/discord.yml | 2 + src/main/resources/plugin.yml | 49 +++++ 60 files changed, 418 insertions(+), 374 deletions(-) create mode 100644 src/main/java/com/loki/lochat/api/service/pm/PrivateMessageService.java delete mode 100644 src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageService.java create mode 100644 src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageServiceImpl.java create mode 100644 src/main/java/com/loki/lochat/integrations/discord/DiscordRateLimiter.java create mode 100644 src/main/java/com/loki/lochat/utils/persistence/FilePersistence.java 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 da3260fb379f98726d0a751fcb4232df8baae4c2..912f70f9bcc2e04926e2e1e05d597957280e49da 100644 GIT binary patch delta 27 jcmey#@snf2VP;M-1{MZ!22KWv$!D0e7-c7$vp4|&ZaN0e delta 27 jcmey#@snf2VP;Md1{MZU22KXC$!D0e7^Nqhvp4|&ZPo_H 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 b64736a1bdd5d31681549886476cccbf24efc4cc..049ed8f15d322333bc27bd40f2821c3e5772cbcd 100644 GIT binary patch delta 13 UcmZ3>x0Y|i12#sf$q(6l03@OW?f?J) delta 13 UcmZ3>x0Y|i12#s<$q(6l03@9R?EnA( 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 c6624cce27363798e9dca6ecc851d655391d077e..676df0630015bcd90cb08dcd3df483a7e39095f2 100644 GIT binary patch delta 27 icmZ3;v5;d!HZ!Ln0}F!?11E#<=;hcjhnZ85frUYcfs;Xa@=|7RMv2KEnPmV|#0Cfe delta 27 icmeC>=;hcjhnZ7=frUYkfs;XK@=|7RM)AoXnPmV|k_H3- diff --git a/bin/main/com/loki/lochat/commands/chat/LocalChatCommand.class b/bin/main/com/loki/lochat/commands/chat/LocalChatCommand.class index c784e17b242abae0698d9206dd52a74e72b48d78..be70917ab3ee5ebadbb556e537c40d8ea8c3f5ee 100644 GIT binary patch delta 13 UcmdnPw})@Tdp1Vd$sgE!04BZ#EdT%j delta 13 UcmdnPw})@Tdp1Vt$sgE!04B5rD*ylh diff --git a/bin/main/com/loki/lochat/commands/messaging/IgnoreCommand.class b/bin/main/com/loki/lochat/commands/messaging/IgnoreCommand.class index f662fc2c9fcd349df61019ee5e78298512ac3c8c..bdfbbbde821a1676f9828715eecc896d51743a42 100644 GIT binary patch delta 17 YcmaDS@=j!fA{V3dj6%1e-k^0K0U!bD?dr6xvgY&09QAxjX=Vz96v{(n01!E9wX6|{+nYoYYkL2A;CU|*%XxcsN%p@ds_id|ZT$y`jw`(-b z`{8;jLI}5vo>6NXon~#@wlC}e7RY-obvluCYK0~>@>VU&2YQ=sH{?4VR3^8_)>~^L zI62@m8-rJWUkOhSN*9eGQU$Qmsz*@i#MazkAs)TBZu4!hwKc80xR-R<4k-t&OsdFqz-wU ST#lK76&F`gW%hAhMEM(?*lVu< delta 547 zcmaKnzfJ-{5XNVh<2aAMq96#UCn(sk4T(lHRu(3bkeHZIn0Us^!+K~YBo+Fex_gQtfSi|+jX3- zqrfx?Zt_v6%#X-Ae~|fLom{lrmdgy=wcI{ycJ#jC)c^VUF?ZlPmTlJ84izX8 zVxhqeek|SbkD&13d*M=8ihwrR}Y2GjXjZqkk2|+RI6ABXu z`3ZzYoLB-$pmA#&;X=@8HPT83NqEPY|tw04;+_L%w DKU`X` diff --git a/bin/main/com/loki/lochat/commands/messaging/ReplyCommand.class b/bin/main/com/loki/lochat/commands/messaging/ReplyCommand.class index e76663c86dde2a2239af0449a4b8b655edcd6a13..d484c1e90c2de305d3066db5fab0e79dcd653d2b 100644 GIT binary patch delta 556 zcmZutO-lk%6g_Wdbo4bjHl>qhshMA!+!z&+(867T)GAsHF^EzoW4H-w*D{btzr-LY zDBQa02ej#rM0Z9bOgHD;uXE3R@5_8L`SAPY4Zs14Jw8YT+m|P{(`}sF4CZ0YX>_e? z`^fHeth(JB?8Fur^rqFdN*7kEUaC6Il@q`s^Tmax=EW}C7F(p|8S}6q6qMntC>CSlsez zAF0 zy?Zkzp`T^|1F?N;EV<09-yEe7Z(E`gD?*RH-pIL7&bdb7CA=dqRsQzqft~%*5PvH zRc2%m@J%f)PE60tOP@TGOR=7bL5P8!fdgp25CbOz7m#LRU<4Y)10;EYJQD_Hpa=s4 z*KP*BNFjmk3_|LXD+3lp zCwUnPAvUmr^nz`W#bSdz78~S&IusaKKvn}y6$4rhqJS<1QOqE>fGlGIssWLT3@Shx H2mJs5 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 56ff27662b3f8c26fff0c3bfdcc94a3040377ace..25c9dead13bef2581f487d5b2246d3e71d3c1a36 100644 GIT binary patch delta 23 dcmZn?ZW7)wg@aLTvH_>~~vxp&Md|RAX|L0Hta8o|%a?ZL=eD2-D;$woJ#O%(BFiRNvI%;>7gS;MAhB%;Z!%9fbgF;-0w$ zIhtIYsB)Y0S*Nq}Dl)Jz$T9FUC^3jIC{GsV^=4F^9M5acs6BZ)uMeve0}q4q(9W=5CGH_$e_RwJoztQDQh?bCqu;Ka()*89z-v` delta 106 zcmeBH+%3Pso|%bd+Ga=Q5T?m#tooa$usN~w$}zAoNHg#=$TNsAC`^9L>&>V%S)b3G zQDbsCpAV}I0}q4kWJ`YS$&dL0SzUlUS0JyQ*OP&p!3(I%n?ZrWck)^OQr18QPKKb# H?gB0V09O{= 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 a981a8e532c5f0cc60f0aeda8df46f3f45cb90c3..c0bf267d1c2910e62dd85b29cd517b09af41ec4f 100644 GIT binary patch delta 218 zcmWlTyJ`Xf5Jk^|Mb<*aUC~9B#GnyYg`k0`0bdYbY@y-2ou31N=(M$g?HlQ zB-teBl4L@PFJ76F#v#L;Hx@#z$+9QUFCQE!aH7an_KlLFQN|C9aDx&+LznBGY-T9n v?EDwwQyi*zsS%{kGYv{KsbD(?ZELoq+m2}4fCr*7~2f_`VAAS4}Kcy-Q0UWIN#-jZs<;a4LwPOLv)U&e@38v924b)15QbEhC`MM&T(ay zx#X4{o+O_<1Fm@InjtqR3Vc#zOo^G03n5oj_@T<0JGQd_sc9DXm_VNxXbCKAN&CxV yga&GEZc-28&>&7z#9Cx%ORGbjN42!Rovw7BkU&PB7pI6}mVS4Jze%k@i*d-+Z 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 9cc7eb3d1957d25a41c5c83b26127154f80994c1..c254b343870821977a4856eeba1dc52689c7bc00 100644 GIT binary patch delta 85 zcmZ1@wMJ@#4mYPD0}F!?11E#tPj7pQA@OS_K45tkh delta 85 zcmZ1@wMJ@#4mYO&0}F#711E#fWK-@OK`|g(oI#91fTDF#I#RA5kHP@3$@6Urz*c{Yy+qvGTzJRSfE-VFo* 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 a24cbdaa6a21f82e517baa06722b12f37864f2c7..3f4a06f070d8e630748c2f415505b5e4b47f8b8e 100644 GIT binary patch delta 97 zcmbO(JzaW3Iya{v0}F!?11E#<D26+Y}1_cIt x2F1zzJROp%3>*w<3<5xTVW7Meg9d{lkXB*PoVOm8?OhW#$=_g$ t^Ye5_sxWW>{a1R!K5kB71{MYp22KXi$;Y{S6(t!28KfB`7-Se!8DtsM859_d7!(=I8I%}o z8I&2k7*r;g@^naQFmNzvG6*neFbFefF-S3J1J!6Ts4(bE7UK%pi!c`L66 E0A$+`KmY&$ delta 101 zcmew>{a1R!K5kAS1{MZk22KW%$;Y{S6(tx18Kf8_7^E3g8DtpL8RQv^7!(-H859|8 z8I%~j7?dZM@^nb5GjK3yFbDwUg&8z~>a~FKnhYuo+LOh2Lm4$D=kR(kYE9nC>j3~@ Cz7Q(_ 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 846a4010716e214e3746e1a811a30762007b895f..221f72bc0fdac6c49d589612e56e27d8a812292a 100644 GIT binary patch delta 201 zcmWN~y=uZ>0EOZAgAT@}7-A&`srZAbNGim%r62~4ifvUKv{YNY0tY9TUV{jk9PAwk zgedw%8Pk4KKO|EDZ;D#FASd1}JO6BU^rbupq5xY}Ci sPm+A*k{4pMCEUTKE6JXJivA?}gc+dnipCpRhU6L1wNSDhDsk)Tp=qa6%|3-(Ba(ai n?HhIw(L+^7kvwky4!)0{=yNJ@ChOd+3k8>UH8r%_pF_9*pS>fC 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 bee84506ab62edbce66e7b4807b6dd03c331354f..f8f403dbdb579d3b3016cf19012ed858f70bc7c8 100644 GIT binary patch delta 85 zcmdldwNGk;9XF>S0}F!?11E#TDF#I#RA5kHP?{Xh6Urz*c`c6zqvGUmJRSfg6b+OB 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 0ee81f1bd9691ed2026a9299244f4fc9b47ac63b..4860e67267b48f1e10b95648d8d9f218890f85b9 100644 GIT binary patch delta 97 zcmZ1_y-IpR6*s3K0}F!?11E#<D26+Y}1_cIt x2F1xrJROp%3>*w<3<5xTVW7Meg9d{lkXB*PoP3xklu>Q6B(Ddf#^fYk4*;yC4h{eS delta 97 zcmZ1_y-IpR6*s2<0}F#711E#f=_g$ tEAezlsxWW>nCo1Q^s9gc&q|YBhm!8Vo87T9f&CLmAa4r}BC*YEE9q>j3~w Ca1YD? delta 101 zcmew+{Y`qqUT#hS1{MZE22KW{$;Y_c6~%yT2_TeYP-T!}P-l>3&|#2cFl3NtFlSI; zaA8oKoX684smj2?pvE8oR3i*jBgLS>pa`T@7&IsI^M*32O-|+YVAPnrj@JVKNmCEE 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 c35e7713d08552639a525bc5b0da41d9605b5110..c113170908ba45f26e0f60181d9ea19d7efb29c1 100644 GIT binary patch delta 247 zcmWNL%Sr-a7>1ueS~NyRj^ik0P?m)i)=)_Y!a`9oEuxT^P6!<+r)b@xix`M*ppC0Q zyoPY$&N~pdVz?FR+r99fo}SgSzWcfsU;h9iZ~~%a?Tny>)f(Ig-xE>;*D)SB>Bl6Rg%=%=SNx%(zM9XCQFAr zT@LxBpga_nm%P$BQZbI1t}OOM$136k94oy|hgWHrSSc{gm2vHSb>9l9$Sij_DG97BM=L_98g|#` aat#Z0=DC-j9)#NvW|MOvrD+>u*V6>tm@GE{ diff --git a/bin/main/com/loki/lochat/gradient/listeners/GradientGUIListener.class b/bin/main/com/loki/lochat/gradient/listeners/GradientGUIListener.class index 37059d423b54b8b9fc662dae0a135b82ccc1de9c..181303d2f2c233d2edae97231d28ff01cb8ab599 100644 GIT binary patch delta 253 zcmWNLJ4-@w9EP7CUErL^8amoJhmf7>+1;z*A7b zmvIOaVucJ5Hi^_3!vLL7vT`(i4NW;8EPM%Jb4K2}SXo$9)|6@zD zlhEqg5RQI;pbubUASC{$=jC~M;q5wIr|}aQ4Kc(7CPBur)NjBsCK)7xK~yaVzouuf zm?g|SQ^ZITXNN_qB)DaXCssbJP*0v$8OFiKI({~oV^fz~tg)?al0!YplH#6S9@wMC zK5yDKIpUKvEi$w@ro#z;y+V3}2REk(0}F#F11E#ny+V3}2RElM0}F!)11E#%V!Z diff --git a/bin/main/com/loki/lochat/integrations/DiscordIntegration.class b/bin/main/com/loki/lochat/integrations/DiscordIntegration.class index 89a0389f4d96170111bef5038b9d84bb926b055e..09b441b6792ace1ddc0d36933366f3f6953b7a36 100644 GIT binary patch delta 215 zcmWNL%Su9F6otPaI5&PTC&e;@uF$~?1_ebzR%BEt1zk{1I0!KZ;XLpO9PYTV*`UED=WKJw4lnGI zvd1g?Oi1j_|0veTD;LvL?m(?WsvOy$KXb=&pU8bGQ_H5oyE_wUi(H6YiX`U3w(E#q vi{9wltqsDl>*CSF=U(my`w2(Eqt-rY@3Xa{PSDqVpxvp>qpPK%{YI64R-Yml delta 215 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 W{rjN){eXOs)auUgh&(1AM&yU9zKZ1l literal 8791 zcmd5>3wRXQb^edE(yUek5-TKF8)Jcm(F3tSFaZg0Kp+s75Fh~tY?EO%k``9G>)Ba^ z;@HNm<1`N^u@k2@NgA6v#SSEKY_ljhja|1TZR$RqHcek?-MUR*?)wp!hWz)=?6QKz z!C#Yn{XTs=ckY~f&OPV+=iGCTzVZFDUjwjC?lVv(Xp5!BqKVX@IDcZJmK%*HU3)MFKXA~Fxq`}cB0CaK3U264IU~^n z*+YloZq%EMYVs{yvrQ9q3PR%^yPZx#5bnLmZ@_ip$q`Znvl-h-T4T1LiYr~q(X@se zAG1?gN|YYKG{<(GiFK}PkEPuV+3NQVcEXwzmmtuU8nQzuM}vtlW(mrh zTXu!%nuv*Oa4l6&+T(+xj%^K5f##OdDtH9lR*ZU1bcS#pt~W4OFjt%PN(yI8G-94$ z7DWuu<_X*5lQ)QGq1YFoR1O3r`?2spc+L@RC%MNCPLom9VV(_s;@VhnC6Y%X<|BR1U0?!q`fse zcEEN9tpf?dEZm!lS&3bi6Ibv4WWXJb6IkuNZzD#Cp=krI6+6^tr8OG^MAN(<8W)1< zjGY|nvojfMgd1Az+rL=pD_FZLHI`1;uG)Ba*3CK!$5jLIk))+w1l#`)tX3BRdVMOD zu&re0+D-!-1lK65jHFToPjn=cN=CQv$4dzzbfF=LO#*|_<}%dOOpl2zxP^*ZLqpq~ zRN8i2M(V;6yDJjv`@3aV5LH2JWte*jVSm<1gy`IDCbnaTZ#_Rx3A&rL&r0vE^KIBM zaF9v~=DZa2cZ85c+CWM$Loe3(vq#l5wqxQ>(9^-Ry^~ahMtGu$9Q#{lkj5#}CP3raW`Ia73lHAD+y-Bd1d(vzS`;UK4lY9-jQnh=OG$ zHdE4K8x!^zbE@WZl&hHvgZL??p#p@1xL>`pT*|oCSUQ9U@G}P9pErv75ba6wTas#i z03RfiWNKVcpFeAhDX8HC_^^ow)u9}8Q(j6o@KHgeRKRJ6hVT(IsGoo1+5B8!M@VVz4PX(GkR%=hMBdFnfMewO*@R(#Kov5?JDcy&XQg# zv9;lBGN!UlbRe5fC*rn4)BYSDH}F|jcqOrZS{;Hr#MWk%S}B@fcqC@)6q_+{LQtpGi>IPJ{e>?fJdKkE zKF{QoQ-@FD1g$$zMS;K8-J#cm`iqh)M0-(aW+jk7bXj zmqgFuw1MZRit9Cz!x_PJ79`28R5HmcjXoHzxn%vtZ>)d5!uqGM=gqF0rpSu~LA=27 zbzA?yUn7RbG@ZN z6Vq3-sH#g8^ItV_P8C(*i_G)2RS>^MKkVdHs(sxEIR)_z-V1tDUA*SC`!~CGof^IC zUUcc=C5q)MCVmsYrAsPzL#jXPrqz`z(7d;0m-_rWCVm&c$5cXzzRH97ds%=fQWJgC z#P2Jj>3Un%kWZo3f6K%l;@iX$Bh6#ZgkoE)^k?TSPM8#b>8!#Z<4+Cz2@7e@=p^7} zm08@4ve-@zSn2)sDA1>?B5WGR}~Z6tcgTQ-I%8r zXNX?ru7J{6mGW1PTT+#!D51flxfF()hq9;<&+M`iaY~_W_Yy}^XUa^Op}%ZoJ7OXz z4Fu~b6GNsW8r24Oh8C;S(n`mpzRfaGwRE;A5j;t^P(R1bC}o3kEsLK^Rx|obGq}!_ zxytjE2|GFBjvCTP_h==(Iwg^?N36s;XC$kthC=pblZ2`?d8)o8CCw5{SYrobD8PD3>l3 zFB{Z7x~E9zypb-SQda9WWs_`XF3KcqJFRj%J5uVy7E^8!o}JVD4w+}4OM5WB)zpHm zru3_vQl;94j+0HhVKxaXRp{?9Wk3aYWyU^iJGLP^^Ia(qZ21NDP(ee+kLC6A80AG< zFA&gEjYae7@X|tyc#tGCkjgqSyF0FKdv$)I@~f7`DliC3n%vE}j+OR~;!H?xm;Hv^ zF(q{3$-}8bcC=SxpM=)1*Jm^3-9jeSV;83cuM`8F>UL&mLaZ_j{TO-b~4l{`jryJ7Zsw zH>TF`#XBvwR4OREC^lYpBHHY~C3zagcno(m@=!a%Ht3~PKHwXrahSaadq@+$hds@1 z{o3fJ8dcGK8Fy=#*yk#LJxyfx@Jx5Q5*Dk4j3IZk(|Yj~m;#-tfHdUZDUn%oDrc8E$fh;hxEzI58&*Hrgw;>BVD*y?Sp8)CRX^E$)lW8p`K%_VFzWa_6ObFp zwTiQwTF;=q^(&LOlYhZ|2Yy`09P*7nm_+!_g-#*)_hiW%kK z9X*X@t@V|svAngxIE@uwCaZF+&NS0% zj@O{<0=Ez}AO_YLSm^x;OCe6k1(KBdR;K9}(poojn`Kx>x-zU+w!=8Lpf=I+0=o8` z!N$Ht;qJB^HZKbIwmy%=deq0!5{_DF#r|jXVr88Mz0-Gc?<={(RftgE8>q3eK%>XB zV1X9u5#$Df-2MOtDPFJ>y9%4xtv6#)Po;fLYirx0_DDc-xOIxiN-hJnP$HIlqy-T* zxJ`@Ta@@|5GI9;>;7I9LgZ&)QPIx!0g4hFEY=UByscU_)k&4oCH)y$QJ=%iY+JfBL zg527I+*)5Qxg&-l?pajRiNDJFEBRC!g=pdVUjaKs3VdZ6d?Bqt50Sk^%QZbpUv8P| zlzLp`A?l^@f7171M`hbNR7Wbq2el`?@gX{IbNe~eaX!&rTRVxdNn~;uk5rt4$;tO9 zweN}q&S7qzj}bA#_eKI-$nW<>4E4P-{L_wB z_tJ?m96*`|ouE07vhDW)jN)O$@fjS%Q#hneHq6MPSqiQ?S#Z_Kf~!szTy?VGs*}E} zs&E{i1^ zl&x)N@fe_dK8ep<45;ZEsSa5`z(n9^6&k!H1r=(vNM1HnxyawoQ9*_3Ur-{-W|D)- zpRbX(N_mrbDu*xjaro>@nB3YPKAXc=+ruw~U(VrI+QVND|GIk8!N1YY70Yt?ZU6bI zbOfZMqCJN{=%|cTwmpkK%Hhv*_^U|eIjmC7{<{-c+7VE%LH!yDyx36@349%)NJU5B z1ZG46k&4gpjgK7uk)O%z-&O?b12gc-e+5qQO}_9rv-b07%HhrM_jCNG8NZaD z@@MJ;XN4)DFx_FO&k^HAMp7U4V3^u&V!JiVX9L~!Z9bpESM~E{ysDob(1r;s;Db!3 zA0p+0n8OZZ6F$mR{xPQRhiL7GnHfJ$uAd;^NBH|m9HY~Z;|W^#QF1-TF43pR`w6^+ zPvd3w-(KbTTlg%cJkCDD6Zj6PRc`Wf#A^kwy(TKz^YVgk;;5*|Zon&2EhhcdhXpcC zrW0hxaI@4f?FTfVHlykS)ib>0ZQwZrlLp?Pr!g0wM>7L)8T)g~@kJhFU*d{q zx#l?r_4CYdIc+!Z*y#1`HkZfiQEj))yxq@}`P{oY25V6{Q_kZ$o_?WChFo7(8&Y+q zuVFv!r^2l61zE7CwN4hEk(S=JKu#95T^edP(AZ5ZC>9ijTAf5?377Lf#IjVDL2AoX zW$H1PYAUEs>Udzy3Dz4l$W1v}@ffZPOv=h^teBLI zCopa6r1a#Z?*yjbUnctVlNZkTOBL~1*``)*FIPONO9ky%FKcyiv04Uw8?Rux1Ty@Z zr9}=iaUP+qCs>r+MasLW(mk~Ey-b`(S(s#)knU&NdY@Lc)l3imA%Oi$O diff --git a/bin/main/com/loki/lochat/integrations/SkinsRestorerHook.class b/bin/main/com/loki/lochat/integrations/SkinsRestorerHook.class index fca0896fac45546946608c708288cfd221b4ffc6..6099ae870508c71a394359e75767bc2c2ccb3079 100644 GIT binary patch delta 133 zcmX>mc}#M{NlrmA1{MYp22KWX20jJ}1|bH?$+tMKn94G6Gspq?@(d~r3Jj`1QX5E` zF{m*(GiWfxFlaF(0!4BebQlU5bQx+H^cb2L3?@J3n#^cCxtzOM)|P>t!47Du6N3^^ dhZ=(mg9(ExgCm1GP{eDpI8PR1=;S6I7XYi17B>I@ delta 133 zcmX>mc}#M{Nlrmg1{MZk22KVs20jLH1|bHC$+tMKn94A4GsrSXFvu~eFvv5gGAJ`> z16gK3mNSDoLkxo^Ln4C~LoQIXkU@u`mO+=Hi9vtzW3I`JMw83An`Lbn*cohrraCex dF*q@(0d<)$xBz)>K%VDhah@#3kjYIvE&!w&78(Em diff --git a/bin/main/com/loki/lochat/integrations/discord/DiscordConfig.class b/bin/main/com/loki/lochat/integrations/discord/DiscordConfig.class index 55da43a61d58a1f8a8054d4cc04458487898d66f..3139658b0612d60633b997c9887fdb41a87f8e0d 100644 GIT binary patch delta 945 zcmY*X$xjnu6#qTTGMz4Tr4pNpLI@Pw#KeS)MnOnCU}ErOVlcIgr4d?f2SYq*!hi5i z?zkd~A_7%W!3FmP+*jOp6ZPoH`1NaTa1L+Y{Fe9q=FRM?obszT|BQ?RsKxgIm6Ua! zh^8+Obv9`mJG7*pT%jdaYToXGN~APl3AHY z!+b$;(FRL2EuK*DfyYf1 z^qIe#{CQsqKK^0yagDj0N(NV(dkns95N{)vS%W;$0T%xdnjPC+*qOraex7ZcWwD?b z9zhZIVJi<<-39w?Y6@K(vUz!4kxB=7tjI%$xZCEZ!?N!cy^DX_%M`^?2DsMhq%}OJ$W4P18*-#17EWW8r*S5mFaiT+h!}Ha!jZfb)^9^``k{IR z=XkT-YmC5xR`?L%J$8@dJVu2zA}j__sc6S|)KOx%0Ptd~n|C>88Z#2YMaf*sGC!v= zm(xuCIFL=H^Dl=d=er_(S2I59{ExfFHm4{5y2#nZL(cMLgqzr!eV3YaBoQpL?OXQ3 zNulZ!zzn2+?gB|}fm`(Ej4<~N?#N}{O)rzW*lu+;7%(ZF_i$g_D}?*0@H~_Cg{)Ju VzLxc^tncwrXjP&6gfIAplE2)n1FHZ4 delta 791 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 diff --git a/bin/main/com/loki/lochat/listener/PlayerEventListener.class b/bin/main/com/loki/lochat/listener/PlayerEventListener.class index 96fbbf51a7fd34da633705a45e0fce40ec07782f..68f82381491c77b6722b066b7d2aa888fe4a67b4 100644 GIT binary patch delta 73 zcmcaCby;dd3pbkp7M KAI!4j6$JntlN7fA delta 73 zcmcaCby;dd3pbkp7M K7tFHb6$Jnri4>^- diff --git a/bin/main/com/loki/lochat/utils/format/ChatFormatter.class b/bin/main/com/loki/lochat/utils/format/ChatFormatter.class index 338731bce0cc1d6e0789fca87bfe49cd31365f27..fc6dffb456cb7d8736467c90e9fc3f85077a922a 100644 GIT binary patch delta 359 zcmWO1%Su8~6b9h`KvpM`6g}#(vo*UJ8C^_}BB4QMWEY|=FdP(S2^Mx6n9jn;kQ%I< zf*BMMM_#`CDOvaDLZ_5xlx50u$_vU%$}7ri$};7%vjQDAaHEPKYV1~| z+}~EWe7Oz-cL?B~od*B?fEa5Wk1}SJw3f82wDuxDRz`c3xXMZ};F32L)2i~VmbID; G1WNx9YD7T* delta 359 zcmWO0%}N4c6b9gTAghVY%*OF&MzfO2EK-7rz!rrTs0E2Flqjr>NX%-XMw{p#gs4*x zNb9y;K{wD9w2ZDJ=v|$UbDr;O^e%{=)izvv{k##mj~VO!4AK? zYuDZ&J45KgFm#L{!a9jj%pfdDciWucG(sOm52oP2Gz``;&ZCIQpZi|5DCYQoULN$E zvLL^D*}2Hx5_?Iwkb;R7DR_!%hI_Kyu_m9M4RxI+N0S%Rn^uZ);5`p*G4VEfQDV9s z=GD-PhJ9El$(L_gvcApGA!UWKN_j+CqdcZOp**FmQa(HDP|?63nh4^I)4J6B zZS#VcFQMTI0o-tMi!kmm%W9z|<3>dhc`?$;tNa*wd49 diff --git a/bin/main/com/loki/lochat/utils/player/MentionHandler.class b/bin/main/com/loki/lochat/utils/player/MentionHandler.class index d75478031a8e159f0d96f7f57f8f93fe7b2757c6..d54d83fd8f6e25aa065531c2cd62bad5b56cde76 100644 GIT binary patch delta 244 zcmWNIJxD@f7)H+<-Ac2lxT!ZOsi2r)-k=NQjU`<5AJ1s_pwWIs!LjcL}g*&xa$ z+iZ~}#-7pvJ5-5NW0yJ!9<)5-(k8_dX*y)+k>!OP1AV-+&xi6S1-`WZaL6x3MwIxY zEMrt;o+F8HEHR3b;6!qMP9@KolsNY$LS>PfsfKT@YXa2GP^rl*_h!|(vM!k9(tJCM VrtjpfhT4r<#cHv@ory0s{sFhgFINBn delta 244 zcmWNJJxD@P7(mY#z0$I%xTY_Sl*)?Iih4;ZBxGg|y`Z7q;!i|B(CF@5GzhmTXl)K2 zgmZIqV{OfWqf2)?hXbeMbey+WHKqs + + @@ -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