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

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.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);
}
}
}

View file

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

View file

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

View file

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

View file

@ -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<UUID> 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) {

View file

@ -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();

View file

@ -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<UUID, String> 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

View file

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

View file

@ -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<UUID, List<WarnEntry>> 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<UUID, List<WarnEntry>> e : warns.entrySet()) {
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
@ -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<PunishmentSnapshot>() { }.getType();
PunishmentSnapshot snap = gson.fromJson(r, type);
if (snap == null) {
return;
}
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.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()) {
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<String, BanRecord> 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());
}
}

View file

@ -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<UUID, Set<UUID>> 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() {

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.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<Boolean> sendWebhook(JsonObject json) {
return CompletableFuture.supplyAsync(() -> {
for (int attempt = 1; attempt <= retryAttempts; attempt++) {
rateLimiter.waitIfNeeded();
try {
if (sendWebhookSync(json.toString())) {
return true;

View file

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

View file

@ -29,7 +29,9 @@ public class DiscordMessageService {
getAvatarUrl(),
config.getTimeout(),
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());
}
}
}