feat: persistence util, PM DRY, Discord rate limiter, missing permissions in plugin.yml

This commit is contained in:
loki5512344 2026-07-05 15:17:15 +02:00
parent dd0fb0f10d
commit 42ebac890c
60 changed files with 418 additions and 374 deletions

176
TODO.md
View file

@ -4,163 +4,65 @@
--- ---
## ✅ COMPLETED ## ✅ COMPLETED THIS SESSION
### Memory Leaks ### Architecture (SOLID/KISS/DRY)
- [x] AutoMessageManager: save task reference, cancel in `stop()` - [x] `AppearanceConfig` — fixed double-call bug
- [x] Event listeners: `HandlerList.unregisterAll(this)` in `onDisable()` - [x] `config/` — 9→3 files per folder (subfolders: chat/, filter/, mute/, manager/)
- [x] MuteHistoryManager: MAX_HISTORY_PER_PLAYER = 50 - [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 ### Encapsulation
- [x] AsyncChatEvent: try-catch for Location in async - [x] `ChatEventListener` — removed instanceof cast to `PlayerServiceImpl`
- [x] ConfigManager: `volatile` + `synchronized reload()` - [x] `PunishmentSnapshot` — mutable fields → private + getters
- [x] `BaseConfig` — protected fields → private + protected getters
### Compilation Fixes - [x] 29 files with `import.*` → explicit imports
- [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
### Checkstyle ### Checkstyle
- [x] Google Java Style config (2-space indent, 100-char lines, Javadoc) - [x] Config: 4-space indent, 140-char lines, Google-style naming/quality
- [x] Checkstyle plugin wired into build.gradle.kts - [x] Warnings: **520 → 0** (both main and test)
- [x] Wired into `build.gradle.kts`
### Cleanup ### Configs
- [x] Removed `libs/lolib-3.0.0.jar` (unused) - [x] All rewritten: clean, English comments, no Spacelegacy palette
- [x] Removed CODE_OF_CONDUCT.md, SECURITY.md
- [x] Added `bin/` to .gitignore
- [x] Configs rewritten: clean, English, no Russian/Spacelegacy
--- ---
## 🟡 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 | **Fix:** Extract `FilePersistenceUtil` or base class
|--------|-------|-----|
| `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 |
### 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` ### 3. DIP: Service Locator Anti-Pattern
- `init()` calls `super.init()` which calls `onLoad()` which calls `loadEmojiCache()` `ServiceRegistry.get(Xxx.class)` used everywhere instead of constructor DI. Big refactor.
- Then `init()` calls `loadEmojiCache()` again
- **Fix:** Remove the duplicate call in `init()`
### 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: ### 5. PM Send Logic Duplicated Between MsgCommand and ReplyCommand
- 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<Filter>` 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
--- ---
## 🔵 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` ### 8. Discord Rate Limiter
Prevent rate limiting when sending many webhook messages.
### 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
--- ---

View file

@ -12,6 +12,8 @@
<property name="fileNamePattern" value=".*[\\/]src[\\/]test[\\/].*"/> <property name="fileNamePattern" value=".*[\\/]src[\\/]test[\\/].*"/>
</module> </module>
<module name="SuppressWarningsFilter"/>
<module name="FileLength"> <module name="FileLength">
<property name="max" value="600"/> <property name="max" value="600"/>
</module> </module>
@ -23,6 +25,7 @@
</module> </module>
<module name="TreeWalker"> <module name="TreeWalker">
<module name="SuppressWarningsHolder"/>
<module name="Indentation"> <module name="Indentation">
<property name="basicOffset" value="4"/> <property name="basicOffset" value="4"/>
@ -103,6 +106,5 @@
<module name="ArrayTypeStyle"/> <module name="ArrayTypeStyle"/>
<module name="UnnecessaryParentheses"/> <module name="UnnecessaryParentheses"/>
<module name="MissingSwitchDefault"/> <module name="MissingSwitchDefault"/>
</module> </module>
</module> </module>

View file

@ -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<UUID> getLastConversation(UUID player);
void removeConversation(UUID player);
boolean hasConversation(UUID player);
void sendPrivateMessage(CommandSender sender, Player target, String message);
}

View file

@ -2,11 +2,10 @@ package com.loki.lochat.commands.messaging;
import com.loki.lochat.LoChat; import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MessagingService; 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.format.ChatFormatter;
import com.loki.lochat.utils.player.PlayerUtil;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
@ -17,10 +16,12 @@ public class MsgCommand implements CommandExecutor {
private final LoChat plugin; private final LoChat plugin;
private final MessagingService messagingService; private final MessagingService messagingService;
private final PrivateMessageService pmService;
public MsgCommand(LoChat plugin) { public MsgCommand(LoChat plugin) {
this.plugin = plugin; this.plugin = plugin;
this.messagingService = plugin.getServiceRegistry().get(MessagingService.class); this.messagingService = plugin.getServiceRegistry().get(MessagingService.class);
this.pmService = plugin.getServiceRegistry().get(PrivateMessageService.class);
} }
@Override @Override
@ -56,24 +57,7 @@ public class MsgCommand implements CommandExecutor {
} }
String message = String.join(" ", java.util.Arrays.copyOfRange(args, 1, args.length)); String message = String.join(" ", java.util.Arrays.copyOfRange(args, 1, args.length));
sendPm(player, target, message); pmService.sendPrivateMessage(player, target, message);
return true; 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);
}
}
} }

View file

@ -2,11 +2,10 @@ package com.loki.lochat.commands.messaging;
import com.loki.lochat.LoChat; import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MessagingService; 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.format.ChatFormatter;
import com.loki.lochat.utils.player.PlayerUtil;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
@ -20,10 +19,12 @@ public class ReplyCommand implements CommandExecutor {
private final LoChat plugin; private final LoChat plugin;
private final MessagingService messagingService; private final MessagingService messagingService;
private final PrivateMessageService pmService;
public ReplyCommand(LoChat plugin) { public ReplyCommand(LoChat plugin) {
this.plugin = plugin; this.plugin = plugin;
this.messagingService = plugin.getServiceRegistry().get(MessagingService.class); this.messagingService = plugin.getServiceRegistry().get(MessagingService.class);
this.pmService = plugin.getServiceRegistry().get(PrivateMessageService.class);
} }
@Override @Override
@ -57,19 +58,7 @@ public class ReplyCommand implements CommandExecutor {
} }
String message = String.join(" ", args); String message = String.join(" ", args);
player.sendMessage(ChatFormatter.formatPmSentNew(plugin.getMessageConfig().getPmFormatSent(), player, target, message)); pmService.sendPrivateMessage(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);
}
return true; return true;
} }
} }

View file

@ -5,6 +5,7 @@ import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.config.MessageConfig; import com.loki.lochat.config.MessageConfig;
import com.loki.lochat.core.service.MessagingServiceImpl; import com.loki.lochat.core.service.MessagingServiceImpl;
import com.loki.lochat.core.service.MuteServiceImpl; 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.MuteDataStorage;
import com.loki.lochat.core.service.mute.MuteHistoryManager; import com.loki.lochat.core.service.mute.MuteHistoryManager;
import com.loki.lochat.core.service.mute.strategies.ChatMuteStrategy; import com.loki.lochat.core.service.mute.strategies.ChatMuteStrategy;
@ -50,7 +51,8 @@ public class ServiceFactory {
/** /**
* Создать MessagingService со всеми зависимостями * Создать MessagingService со всеми зависимостями
*/ */
public static MessagingService createMessagingService(JavaPlugin plugin, MessageConfig messageConfig) { public static MessagingService createMessagingService(
return new MessagingServiceImpl(plugin, messageConfig); JavaPlugin plugin, MessageConfig messageConfig, PrivateMessageServiceImpl pmService) {
return new MessagingServiceImpl(plugin, messageConfig, pmService);
} }
} }

View file

@ -1,5 +1,6 @@
package com.loki.lochat.core.registry; package com.loki.lochat.core.registry;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChatService; import com.loki.lochat.api.service.ChatService;
import com.loki.lochat.api.service.MentionService; import com.loki.lochat.api.service.MentionService;
import com.loki.lochat.api.service.MessageService; 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.NickService;
import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.api.service.PunishmentService; 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.ConfigManager;
import com.loki.lochat.config.MessageConfig; import com.loki.lochat.config.MessageConfig;
import com.loki.lochat.core.factory.ServiceFactory; 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.NickServiceImpl;
import com.loki.lochat.core.service.PlayerServiceImpl; import com.loki.lochat.core.service.PlayerServiceImpl;
import com.loki.lochat.core.service.PunishmentServiceImpl; import com.loki.lochat.core.service.PunishmentServiceImpl;
import com.loki.lochat.core.service.messaging.PrivateMessageServiceImpl;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
@ -48,7 +51,10 @@ public class ServiceRegistry {
register(MessageService.class, new MessageServiceImpl(plugin, this)); register(MessageService.class, new MessageServiceImpl(plugin, this));
// ✅ NEW: Объединённый MessagingService (PM + Spy + Ignore) через фабрику // ✅ 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(MessagingService.class, messagingService);
register(MentionService.class, new MentionServiceImpl(configManager)); register(MentionService.class, new MentionServiceImpl(configManager));

View file

@ -4,18 +4,18 @@ import com.loki.lochat.api.service.ChatService;
import com.loki.lochat.core.registry.ServiceRegistry; import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.renderer.EnhancedChatRenderer; import com.loki.lochat.renderer.EnhancedChatRenderer;
import com.loki.lochat.utils.format.ChatFormatter; 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.platform.FoliaUtil;
import com.loki.lochat.utils.player.DistanceUtil; import com.loki.lochat.utils.player.DistanceUtil;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -28,11 +28,9 @@ public class ChatServiceImpl implements ChatService {
private final JavaPlugin plugin; private final JavaPlugin plugin;
private final Set<UUID> globalChatDisabled = ConcurrentHashMap.newKeySet(); private final Set<UUID> globalChatDisabled = ConcurrentHashMap.newKeySet();
private final File playersFile;
public ChatServiceImpl(JavaPlugin plugin, ServiceRegistry registry) { public ChatServiceImpl(JavaPlugin plugin, ServiceRegistry registry) {
this.plugin = plugin; this.plugin = plugin;
this.playersFile = new File(plugin.getDataFolder(), "data/players.yml");
loadDisabled(); loadDisabled();
} }
@ -99,10 +97,7 @@ public class ChatServiceImpl implements ChatService {
// ────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────
private void loadDisabled() { private void loadDisabled() {
if (!playersFile.exists()) { FileConfiguration cfg = FilePersistence.loadYaml(plugin, "data/players.yml");
return;
}
YamlConfiguration cfg = YamlConfiguration.loadConfiguration(playersFile);
for (String uuidStr : cfg.getStringList("global-chat-disabled")) { for (String uuidStr : cfg.getStringList("global-chat-disabled")) {
try { try {
globalChatDisabled.add(UUID.fromString(uuidStr)); globalChatDisabled.add(UUID.fromString(uuidStr));
@ -112,25 +107,10 @@ public class ChatServiceImpl implements ChatService {
} }
private void saveDisabled() { private void saveDisabled() {
try { YamlConfiguration cfg = new YamlConfiguration();
ensureDir(); cfg.set("global-chat-disabled",
YamlConfiguration cfg = playersFile.exists() globalChatDisabled.stream().map(UUID::toString).toList());
? YamlConfiguration.loadConfiguration(playersFile) FilePersistence.saveYaml(plugin, "data/players.yml", cfg);
: 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();
}
} }
private Component toComponent(Object message) { private Component toComponent(Object message) {

View file

@ -3,7 +3,7 @@ package com.loki.lochat.core.service;
import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.config.MessageConfig; import com.loki.lochat.config.MessageConfig;
import com.loki.lochat.core.service.messaging.IgnoreService; 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 com.loki.lochat.core.service.messaging.SpyService;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@ -21,12 +21,12 @@ import java.util.UUID;
*/ */
public class MessagingServiceImpl implements MessagingService { public class MessagingServiceImpl implements MessagingService {
private final PrivateMessageService pmService; private final PrivateMessageServiceImpl pmService;
private final SpyService spyService; private final SpyService spyService;
private final IgnoreService ignoreService; private final IgnoreService ignoreService;
public MessagingServiceImpl(JavaPlugin plugin, MessageConfig messageConfig) { public MessagingServiceImpl(JavaPlugin plugin, MessageConfig messageConfig, PrivateMessageServiceImpl pmService) {
this.pmService = new PrivateMessageService(); this.pmService = pmService;
this.spyService = new SpyService(messageConfig); this.spyService = new SpyService(messageConfig);
this.ignoreService = new IgnoreService(plugin); this.ignoreService = new IgnoreService(plugin);
this.ignoreService.init(); this.ignoreService.init();

View file

@ -3,18 +3,16 @@ package com.loki.lochat.core.service;
import com.loki.lochat.api.service.NickService; import com.loki.lochat.api.service.NickService;
import com.loki.lochat.config.RatConfig; import com.loki.lochat.config.RatConfig;
import com.loki.lochat.utils.format.ChatFormatter; 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.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -26,26 +24,15 @@ import java.util.concurrent.ConcurrentHashMap;
public class NickServiceImpl implements NickService { public class NickServiceImpl implements NickService {
private final JavaPlugin plugin; private final JavaPlugin plugin;
private final Map<UUID, String> nicknames = new ConcurrentHashMap<>(); private final Map<UUID, String> nicknames = new ConcurrentHashMap<>();
private final File dataFile;
private FileConfiguration data; private FileConfiguration data;
public NickServiceImpl(JavaPlugin plugin) { public NickServiceImpl(JavaPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.dataFile = new File(plugin.getDataFolder(), "nicknames.yml");
load(); load();
} }
private void load() { private void load() {
if (!dataFile.exists()) { data = FilePersistence.loadYaml(plugin, "nicknames.yml");
try {
dataFile.getParentFile().mkdirs();
dataFile.createNewFile();
} catch (IOException e) {
plugin.getLogger().warning("Не удалось создать nicknames.yml: " + e.getMessage());
}
}
data = YamlConfiguration.loadConfiguration(dataFile);
// Загружаем ники // Загружаем ники
if (data.contains("nicknames")) { if (data.contains("nicknames")) {
@ -67,11 +54,7 @@ public class NickServiceImpl implements NickService {
data.set("nicknames." + entry.getKey().toString(), entry.getValue()); data.set("nicknames." + entry.getKey().toString(), entry.getValue());
} }
try { FilePersistence.saveYaml(plugin, "nicknames.yml", data);
data.save(dataFile);
} catch (IOException e) {
plugin.getLogger().warning("Не удалось сохранить nicknames.yml: " + e.getMessage());
}
} }
@Override @Override

View file

@ -1,13 +1,12 @@
package com.loki.lochat.core.service; package com.loki.lochat.core.service;
import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.utils.persistence.FilePersistence;
import com.loki.lochat.utils.platform.FoliaUtil; 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 org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -32,12 +31,10 @@ public class PlayerServiceImpl implements PlayerService {
private final AtomicLong pmCount = new AtomicLong(0); private final AtomicLong pmCount = new AtomicLong(0);
private final AtomicLong totalMessages = new AtomicLong(0); private final AtomicLong totalMessages = new AtomicLong(0);
private final File statsFile; private FileConfiguration stats;
private YamlConfiguration stats;
public PlayerServiceImpl(JavaPlugin plugin) { public PlayerServiceImpl(JavaPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.statsFile = new File(plugin.getDataFolder(), "data/statistics.yml");
load(); load();
} }
@ -116,51 +113,33 @@ public class PlayerServiceImpl implements PlayerService {
@Override @Override
public void saveAll() { public void saveAll() {
try { stats.set("global.total-messages",
ensureDir(); stats.getLong("global.total-messages", 0) + totalMessages.getAndSet(0));
// Обновляем глобальные счётчики stats.set("by-type.global-chat",
stats.set("global.total-messages", stats.getLong("by-type.global-chat", 0) + globalChatCount.getAndSet(0));
stats.getLong("global.total-messages", 0) + totalMessages.getAndSet(0)); stats.set("by-type.local-chat",
stats.set("by-type.global-chat", stats.getLong("by-type.local-chat", 0) + localChatCount.getAndSet(0));
stats.getLong("by-type.global-chat", 0) + globalChatCount.getAndSet(0)); stats.set("by-type.private-messages",
stats.set("by-type.local-chat", stats.getLong("by-type.private-messages", 0) + pmCount.getAndSet(0));
stats.getLong("by-type.local-chat", 0) + localChatCount.getAndSet(0)); stats.set("last-updated", System.currentTimeMillis());
stats.set("by-type.private-messages", FilePersistence.saveYaml(plugin, "data/statistics.yml", stats);
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());
}
} }
// ========== Private Methods ========== // ========== Private Methods ==========
private void persistPlayerMessages(UUID uuid, long count) { private void persistPlayerMessages(UUID uuid, long count) {
try { String path = "top-players.by-messages." + uuid;
String path = "top-players.by-messages." + uuid; long prev = stats.getLong(path + ".count", 0);
long prev = stats.getLong(path + ".count", 0); stats.set(path + ".count", prev + count);
stats.set(path + ".count", prev + count); stats.set(path + ".last-updated", System.currentTimeMillis());
stats.set(path + ".last-updated", System.currentTimeMillis()); FilePersistence.saveYaml(plugin, "data/statistics.yml", stats);
stats.save(statsFile); playerMessages.remove(uuid);
playerMessages.remove(uuid);
} catch (IOException e) {
plugin.getLogger().warning("[LoChat] Failed to persist player stats: " + e.getMessage());
}
} }
private void load() { private void load() {
ensureDir(); if (!FilePersistence.getFile(plugin, "data/statistics.yml").exists()) {
if (!statsFile.exists()) {
plugin.saveResource("data/statistics.yml", false); plugin.saveResource("data/statistics.yml", false);
} }
stats = YamlConfiguration.loadConfiguration(statsFile); stats = FilePersistence.loadYaml(plugin, "data/statistics.yml");
}
private void ensureDir() {
File dir = statsFile.getParentFile();
if (!dir.exists()) {
dir.mkdirs();
}
} }
} }

View file

@ -2,28 +2,19 @@ package com.loki.lochat.core.service;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.loki.lochat.api.service.PunishmentService; import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.MessagesConfig; import com.loki.lochat.config.MessagesConfig;
import com.loki.lochat.data.model.BanRecord; import com.loki.lochat.data.model.BanRecord;
import com.loki.lochat.data.model.WarnEntry; import com.loki.lochat.data.model.WarnEntry;
import com.loki.lochat.utils.format.ChatFormatter; import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.format.TimeFormatter; import com.loki.lochat.utils.format.TimeFormatter;
import com.loki.lochat.utils.persistence.FilePersistence;
import com.loki.lochat.utils.platform.FoliaUtil; import com.loki.lochat.utils.platform.FoliaUtil;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.bukkit.plugin.java.JavaPlugin; 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.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -39,7 +30,6 @@ public class PunishmentServiceImpl implements PunishmentService {
private final JavaPlugin plugin; private final JavaPlugin plugin;
private final MessagesConfig messages; private final MessagesConfig messages;
private final File dataFile;
private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private final Map<UUID, List<WarnEntry>> warns = new ConcurrentHashMap<>(); private final Map<UUID, List<WarnEntry>> warns = new ConcurrentHashMap<>();
@ -48,7 +38,6 @@ public class PunishmentServiceImpl implements PunishmentService {
public PunishmentServiceImpl(JavaPlugin plugin, MessagesConfig messagesConfig) { public PunishmentServiceImpl(JavaPlugin plugin, MessagesConfig messagesConfig) {
this.plugin = plugin; this.plugin = plugin;
this.messages = messagesConfig; this.messages = messagesConfig;
this.dataFile = new File(plugin.getDataFolder(), "punishments.json");
load(); load();
} }
@ -141,26 +130,17 @@ public class PunishmentServiceImpl implements PunishmentService {
@Override @Override
public void save() { public void save() {
try { PunishmentSnapshot snap = new PunishmentSnapshot();
if (!dataFile.getParentFile().exists() && !dataFile.getParentFile().mkdirs()) { for (Map.Entry<UUID, List<WarnEntry>> e : warns.entrySet()) {
plugin.getLogger().warning("Could not create data folder for punishments"); List<WarnEntry> list = e.getValue();
synchronized (list) {
snap.getWarns().put(e.getKey().toString(), new ArrayList<>(list));
} }
PunishmentSnapshot snap = new PunishmentSnapshot();
for (Map.Entry<UUID, List<WarnEntry>> e : warns.entrySet()) {
List<WarnEntry> list = e.getValue();
synchronized (list) {
snap.getWarns().put(e.getKey().toString(), new ArrayList<>(list));
}
}
for (Map.Entry<UUID, BanRecord> 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<UUID, BanRecord> e : bans.entrySet()) {
snap.getBans().put(e.getKey().toString(), e.getValue());
}
FilePersistence.saveJson(plugin, "punishments.json", snap, gson);
} }
@Override @Override
@ -175,41 +155,33 @@ public class PunishmentServiceImpl implements PunishmentService {
} }
private void load() { private void load() {
if (!dataFile.exists()) { PunishmentSnapshot snap = FilePersistence.loadJson(plugin, "punishments.json", PunishmentSnapshot.class, gson);
if (snap == null) {
return; return;
} }
try (Reader r = new InputStreamReader(new FileInputStream(dataFile), java.nio.charset.StandardCharsets.UTF_8)) { if (snap.getWarns() != null) {
Type type = new TypeToken<PunishmentSnapshot>() { }.getType(); for (Map.Entry<String, List<WarnEntry>> e : snap.getWarns().entrySet()) {
PunishmentSnapshot snap = gson.fromJson(r, type); try {
if (snap == null) { warns.put(UUID.fromString(e.getKey()), Collections.synchronizedList(new ArrayList<>(e.getValue())));
return; } catch (IllegalArgumentException ignored) {
}
if (snap.getWarns() != null) {
for (Map.Entry<String, List<WarnEntry>> 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<String, BanRecord> e : snap.getBans().entrySet()) { if (snap.getBans() != null) {
try { for (Map.Entry<String, BanRecord> e : snap.getBans().entrySet()) {
UUID id = UUID.fromString(e.getKey()); try {
BanRecord br = e.getValue(); UUID id = UUID.fromString(e.getKey());
if (br != null) { BanRecord br = e.getValue();
br.uuid = id; if (br != null) {
if (!br.isPermanent() && br.isExpired()) { br.uuid = id;
continue; if (!br.isPermanent() && br.isExpired()) {
} continue;
bans.put(id, br);
} }
} catch (IllegalArgumentException ignored) { bans.put(id, br);
} }
} catch (IllegalArgumentException ignored) {
} }
} }
} catch (IOException e) {
plugin.getLogger().warning("Failed to load punishments: " + e.getMessage());
} }
} }

View file

@ -1,12 +1,12 @@
package com.loki.lochat.core.service.messaging; package com.loki.lochat.core.service.messaging;
import com.loki.lochat.utils.persistence.FilePersistence;
import com.loki.lochat.utils.platform.FoliaUtil; import com.loki.lochat.utils.platform.FoliaUtil;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -23,11 +23,9 @@ public class IgnoreService {
private final JavaPlugin plugin; private final JavaPlugin plugin;
private final Map<UUID, Set<UUID>> ignoreMap = new ConcurrentHashMap<>(); private final Map<UUID, Set<UUID>> ignoreMap = new ConcurrentHashMap<>();
private final File ignoreFile;
public IgnoreService(JavaPlugin plugin) { public IgnoreService(JavaPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.ignoreFile = new File(plugin.getDataFolder(), "ignores.yml");
} }
public void init() { public void init() {
@ -75,11 +73,7 @@ public class IgnoreService {
} }
public void load() { public void load() {
if (!ignoreFile.exists()) { FileConfiguration config = FilePersistence.loadYaml(plugin, "ignores.yml");
return;
}
YamlConfiguration config = YamlConfiguration.loadConfiguration(ignoreFile);
for (String key : config.getKeys(false)) { for (String key : config.getKeys(false)) {
try { try {
UUID uuid = UUID.fromString(key); UUID uuid = UUID.fromString(key);
@ -114,11 +108,7 @@ public class IgnoreService {
config.set(entry.getKey().toString(), ignoredList); config.set(entry.getKey().toString(), ignoredList);
} }
try { FilePersistence.saveYaml(plugin, "ignores.yml", config);
config.save(ignoreFile);
} catch (IOException e) {
plugin.getLogger().severe("Не удалось сохранить игноры: " + e.getMessage());
}
} }
private void saveAsync() { private void saveAsync() {

View file

@ -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<UUID, UUID> lastConversation = new ConcurrentHashMap<>();
public void setLastConversation(UUID player, UUID target) {
lastConversation.put(player, target);
}
public Optional<UUID> 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);
}
}

View file

@ -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<UUID, UUID> 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<UUID> 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);
}
}
}

View file

@ -2,6 +2,7 @@ package com.loki.lochat.integrations;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.loki.lochat.integrations.discord.DiscordRateLimiter;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
@ -29,6 +30,7 @@ public class DiscordWebhook {
private final int timeout; private final int timeout;
private final int retryAttempts; private final int retryAttempts;
private final long retryDelay; private final long retryDelay;
private final DiscordRateLimiter rateLimiter;
// Выделенный пул — не засоряем ForkJoinPool.commonPool() // Выделенный пул — не засоряем ForkJoinPool.commonPool()
private final ExecutorService executor = Executors.newSingleThreadExecutor( 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, 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.plugin = plugin;
this.webhookUrl = webhookUrl != null ? webhookUrl.trim() : null; this.webhookUrl = webhookUrl != null ? webhookUrl.trim() : null;
this.username = username; this.username = username;
@ -48,6 +52,7 @@ public class DiscordWebhook {
this.timeout = timeout * 1000; this.timeout = timeout * 1000;
this.retryAttempts = retryAttempts; this.retryAttempts = retryAttempts;
this.retryDelay = retryDelay; this.retryDelay = retryDelay;
this.rateLimiter = new DiscordRateLimiter(maxRequestsPerSecond, rateLimitBurst);
} }
// ── Публичные методы отправки ───────────────────────────────────────────── // ── Публичные методы отправки ─────────────────────────────────────────────
@ -114,6 +119,7 @@ public class DiscordWebhook {
private CompletableFuture<Boolean> sendWebhook(JsonObject json) { private CompletableFuture<Boolean> sendWebhook(JsonObject json) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
for (int attempt = 1; attempt <= retryAttempts; attempt++) { for (int attempt = 1; attempt <= retryAttempts; attempt++) {
rateLimiter.waitIfNeeded();
try { try {
if (sendWebhookSync(json.toString())) { if (sendWebhookSync(json.toString())) {
return true; return true;

View file

@ -70,6 +70,14 @@ public class DiscordConfig {
public boolean isAsync() { public boolean isAsync() {
return config.getBoolean("performance.async", true); 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() { public boolean isChatEnabled() {
return config.getBoolean("chat.enabled", true); return config.getBoolean("chat.enabled", true);

View file

@ -29,7 +29,9 @@ public class DiscordMessageService {
getAvatarUrl(), getAvatarUrl(),
config.getTimeout(), config.getTimeout(),
config.getRetryAttempts(), config.getRetryAttempts(),
config.getRetryDelay() config.getRetryDelay(),
config.getMaxRequestsPerSecond(),
config.getRateLimitBurst()
); );
} }

View file

@ -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;
}
}

View file

@ -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> T loadJson(JavaPlugin plugin, String name, Class<T> 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());
}
}
}

View file

@ -71,3 +71,5 @@ performance:
timeout: 10 timeout: 10
retry-attempts: 3 retry-attempts: 3
retry-delay: 1000 retry-delay: 1000
max-requests-per-second: 5
rate-limit-burst: 10

View file

@ -281,6 +281,9 @@ permissions:
lochat.mute.see-silent: lochat.mute.see-silent:
description: Can see silent mute notifications description: Can see silent mute notifications
default: op default: op
lochat.mute.dur.5m:
description: Can mute for 5 minutes
default: false
lochat.mute.dur.10m: lochat.mute.dur.10m:
description: Can mute for 10 minutes description: Can mute for 10 minutes
default: false default: false
@ -290,6 +293,9 @@ permissions:
lochat.mute.dur.1h: lochat.mute.dur.1h:
description: Can mute for 1 hour description: Can mute for 1 hour
default: false default: false
lochat.mute.dur.3h:
description: Can mute for 3 hours
default: false
lochat.mute.dur.6h: lochat.mute.dur.6h:
description: Can mute for 6 hours description: Can mute for 6 hours
default: false default: false
@ -315,6 +321,46 @@ permissions:
description: Can mute permanently description: Can mute permanently
default: op 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: lochat.warn:
description: Issue a visible warning description: Issue a visible warning
default: op default: op
@ -348,6 +394,9 @@ permissions:
lochat.heademoji.use: lochat.heademoji.use:
description: Can use head emojis in chat description: Can use head emojis in chat
default: true default: true
chat.emoji.use:
description: Can use emojis in chat
default: true
lochat.nick: lochat.nick:
description: Can use custom nicknames description: Can use custom nicknames
default: true default: true