fix: critical fixes from code review — async translation, atomic persistence, unified chat pipeline, config key alignment

This commit is contained in:
loki5512344 2026-08-09 21:04:07 +02:00
parent 103c982f87
commit b1b0c9082b
Signed by: boba
GPG key ID: 253067914055423B
31 changed files with 530 additions and 384 deletions

View file

@ -42,6 +42,7 @@ public final class LoChat extends JavaPlugin {
// Вспомогательные классы
private PluginInitializer initializer;
private PluginShutdown shutdown;
private com.loki.lochat.core.filter.AdvancedMessageFilter chatFilter;
@Override
public void onEnable() {
@ -153,4 +154,12 @@ public final class LoChat extends JavaPlugin {
public TranslationService getTranslationService() {
return translationService;
}
public com.loki.lochat.core.filter.AdvancedMessageFilter getChatFilter() {
return chatFilter;
}
public void setChatFilter(com.loki.lochat.core.filter.AdvancedMessageFilter chatFilter) {
this.chatFilter = chatFilter;
}
}

View file

@ -4,8 +4,6 @@ import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChatService;
import com.loki.lochat.utils.format.ChatFormatter;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -14,7 +12,6 @@ import org.jetbrains.annotations.NotNull;
public class GlobalChatCommand implements CommandExecutor {
private static final MiniMessage MM = MiniMessage.miniMessage();
private final LoChat plugin;
private final ChatService chatService;
@ -46,7 +43,7 @@ public class GlobalChatCommand implements CommandExecutor {
return true;
}
chatService.sendGlobalMessage(player, MM.deserialize(String.join(" ", args)));
chatService.sendGlobalMessage(player, String.join(" ", args));
return true;
}
}

View file

@ -4,8 +4,6 @@ import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChatService;
import com.loki.lochat.utils.format.ChatFormatter;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -14,7 +12,6 @@ import org.jetbrains.annotations.NotNull;
public class LocalChatCommand implements CommandExecutor {
private static final MiniMessage MM = MiniMessage.miniMessage();
private final LoChat plugin;
private final ChatService chatService;
@ -35,7 +32,7 @@ public class LocalChatCommand implements CommandExecutor {
return true;
}
chatService.sendLocalMessage(player, MM.deserialize(String.join(" ", args)));
chatService.sendLocalMessage(player, String.join(" ", args));
return true;
}
}

View file

@ -58,7 +58,7 @@ public class AppearanceConfig extends BaseConfig {
return getConfig().getStringList("prefixes.global.colors");
}
public String getGlobalSeparatorText() {
return getPrefixValue("global", "separator.text", " ▶ ");
return getPrefixValue("global", "separator", " ▶ ");
}
public String getGlobalSeparatorColor() {
return getPrefixValue("global", "separator.color", "#FFD700");
@ -77,7 +77,7 @@ public class AppearanceConfig extends BaseConfig {
return getConfig().getStringList("prefixes.local.colors");
}
public String getLocalSeparatorText() {
return getPrefixValue("local", "separator.text", " ▶ ");
return getPrefixValue("local", "separator", " ▶ ");
}
public String getLocalSeparatorColor() {
return getPrefixValue("local", "separator.color", "#FFFFFF");
@ -117,16 +117,16 @@ public class AppearanceConfig extends BaseConfig {
// ========== НАСТРОЙКИ ЧАТА ==========
public int getLocalRadius() {
return getConfig().getInt("chat.local_radius", 100);
return getConfig().getInt("chat.local-radius", 100);
}
public int getClearLines() {
return getConfig().getInt("chat.clear_lines", 100);
return getConfig().getInt("chat.clear-lines", 100);
}
public int getMinMessageLength() {
return getConfig().getInt("chat.min_message_length", 1);
return getConfig().getInt("chat.min-message-length", 1);
}
public int getMaxMessageLength() {
return getConfig().getInt("chat.max_message_length", 2000);
return getConfig().getInt("chat.max-message-length", 2000);
}
private String getChatColor(String type, String defaultColor) {
@ -134,7 +134,7 @@ public class AppearanceConfig extends BaseConfig {
}
public String getNobodyHeardColor() {
return getChatColor("nobody_heard", "#FFA726");
return getChatColor("nobody-heard", "#FFA726");
}
public String getErrorColor() {
return getChatColor("error", "#FF6B6B");
@ -152,18 +152,18 @@ public class AppearanceConfig extends BaseConfig {
// ========== DISCORD ==========
public String getDiscordEmoji(String event) {
return getConfig().getString("discord.emojis." + event, "");
return getConfig().getString("discord.emojis." + event.replace('_', '-'), "");
}
public String getDiscordEventTitle(String event) {
return getConfig().getString("discord.event_titles." + event, "");
return getConfig().getString("discord.event-titles." + event.replace('_', '-'), "");
}
public String getDefaultAvatarUrl() {
return getConfig().getString("discord.avatar_urls.default", "https://mc-heads.net/avatar/minecraft/64");
return getConfig().getString("discord.avatar-urls.default", "https://mc-heads.net/avatar/minecraft/64");
}
public String getPlayerAvatarUrl() {
return getConfig().getString("discord.avatar_urls.player", "https://mc-heads.net/avatar/{player}/64");
return getConfig().getString("discord.avatar-urls.player", "https://mc-heads.net/avatar/{player}/64");
}
public String getDefaultEmbedColor() {
return getConfig().getString("discord.default_embed_color", "5865F2");
return getConfig().getString("discord.default-embed-color", "5865F2");
}
}

View file

@ -20,7 +20,7 @@ public class FiltersConfig extends BaseConfig {
}
public int getCapsMaxPercent() {
return getConfig().getInt("caps.max-percent", 70);
return getConfig().getInt("caps.max-caps-percent", 70);
}
public int getCapsMinLength() {
@ -28,11 +28,11 @@ public class FiltersConfig extends BaseConfig {
}
public boolean isCapsAutoLowercase() {
return getConfig().getBoolean("caps.auto-lowercase", true);
return true;
}
public boolean isCapsBlock() {
return getConfig().getBoolean("caps.block", false);
return false;
}
// ── Мат ─────────────────────────────────────────────────────────────────────
@ -42,11 +42,11 @@ public class FiltersConfig extends BaseConfig {
}
public String getSwearAction() {
return getConfig().getString("swear.action", "replace");
return getConfig().getBoolean("swear.replace", true) ? "replace" : "block";
}
public String getSwearReplacementChar() {
return getConfig().getString("swear.replacement-char", "*");
return getConfig().getString("swear.replacement", "*");
}
public boolean isSwearCheckFragments() {
@ -58,7 +58,7 @@ public class FiltersConfig extends BaseConfig {
}
public boolean isSwearUseExternalFile() {
return getConfig().getBoolean("swear.use-external-file", true);
return true;
}
public List<String> getSwearWords() {
@ -72,27 +72,27 @@ public class FiltersConfig extends BaseConfig {
// ── Реклама ─────────────────────────────────────────────────────────────────
public boolean isAdvertisingEnabled() {
return getConfig().getBoolean("advertising.enabled", true);
return getConfig().getBoolean("hidden-urls.enabled", true);
}
public boolean isBlockHiddenUrls() {
return getConfig().getBoolean("advertising.block-hidden-urls", true);
return getConfig().getBoolean("hidden-urls.enabled", true);
}
public boolean isBlockDomains() {
return getConfig().getBoolean("advertising.block-domains", true);
return true;
}
public List<String> getWhitelistedDomains() {
return getConfig().getStringList("advertising.whitelist");
return List.of();
}
public List<String> getBlacklistedDomains() {
return getConfig().getStringList("advertising.blacklist");
return List.of();
}
public String getAdvertisingBlockMessage() {
return getConfig().getString("advertising.blocked-message", "&#CF6679Реклама запрещена!");
return "&#CF6679Реклама запрещена!";
}
// ── IP адреса ───────────────────────────────────────────────────────────────
@ -102,7 +102,7 @@ public class FiltersConfig extends BaseConfig {
}
public boolean isIpBlock() {
return getConfig().getBoolean("ip.block", false);
return false;
}
public String getIpReplacement() {
@ -116,11 +116,11 @@ public class FiltersConfig extends BaseConfig {
// ── Повторяющиеся символы ───────────────────────────────────────────────────
public boolean isRepeatEnabled() {
return getConfig().getBoolean("repeat.enabled", true);
return getConfig().getBoolean("spam.enabled", true);
}
public int getRepeatMaxRepeats() {
return getConfig().getInt("repeat.max-repeats", 3);
return getConfig().getInt("spam.max-repeating-chars", 3);
}
// ── Флуд ────────────────────────────────────────────────────────────────────
@ -130,11 +130,11 @@ public class FiltersConfig extends BaseConfig {
}
public int getFloodMaxMessages() {
return getConfig().getInt("flood.max-messages", 5);
return getConfig().getInt("flood.max-same-messages", 3);
}
public int getFloodTimePeriod() {
return getConfig().getInt("flood.time-period", 10);
return 10;
}
public String getFloodBlockMessage() {
@ -148,11 +148,11 @@ public class FiltersConfig extends BaseConfig {
}
public int getSpamMaxSimilarMessages() {
return getConfig().getInt("spam.max-similar-messages", 3);
return 3;
}
public int getSpamSimilarityThreshold() {
return getConfig().getInt("spam.similarity-threshold", 80);
return 80;
}
public String getSpamBlockMessage() {

View file

@ -14,48 +14,48 @@ public class MuteConfig extends BaseConfig {
// ── Основные настройки ──────────────────────────────────────────────────────
public boolean isEnabled() {
return getConfig().getBoolean("enabled", true);
return getConfig().getBoolean("settings.auto-unmute", true);
}
public String getDefaultDuration() {
return getConfig().getString("default-duration", "7d");
return getConfig().getString("settings.default-duration", "7d");
}
public String getDefaultReason() {
return getConfig().getString("default-reason", "Без причины");
return getConfig().getString("settings.default-reason", "No reason specified");
}
public int getMaxHistoryPerPlayer() {
return getConfig().getInt("max-history-per-player", 50);
return getConfig().getInt("settings.max-history-entries", 50);
}
// ── Права на длительность ───────────────────────────────────────────────────
public boolean isPermissionBasedDuration() {
return getConfig().getBoolean("permission-based-duration", true);
return true;
}
// ── Уведомления ─────────────────────────────────────────────────────────────
public boolean isNotifyOnMute() {
return getConfig().getBoolean("notify.on-mute", true);
return getConfig().getBoolean("notifications.public-mute", true);
}
public boolean isNotifyOnUnmute() {
return getConfig().getBoolean("notify.on-unmute", true);
return getConfig().getBoolean("notifications.public-unmute", true);
}
public boolean isNotifyOnExpire() {
return getConfig().getBoolean("notify.on-expire", true);
return true;
}
// ── Тихий мут ───────────────────────────────────────────────────────────────
public boolean isSilentMuteEnabled() {
return getConfig().getBoolean("silent-mute.enabled", true);
return getConfig().getBoolean("notifications.silent-mute.enabled", true);
}
public boolean isSilentMuteLogToConsole() {
return getConfig().getBoolean("silent-mute.log-to-console", true);
return true;
}
}

View file

@ -14,54 +14,54 @@ public class SoundsConfig extends BaseConfig {
// ── Звуки упоминаний ────────────────────────────────────────────────────────
public boolean isMentionSoundEnabled() {
return getConfig().getBoolean("mention.enabled", true);
return getConfig().getBoolean("messages.mention.enabled", true);
}
public String getMentionSound() {
return getConfig().getString("mention.sound", "BLOCK_NOTE_BLOCK_PLING");
return getConfig().getString("messages.mention.sound", "BLOCK_NOTE_BLOCK_PLING");
}
public float getMentionVolume() {
return (float) getConfig().getDouble("mention.volume", 1.0);
return (float) getConfig().getDouble("messages.mention.volume", 1.0);
}
public float getMentionPitch() {
return (float) getConfig().getDouble("mention.pitch", 1.0);
return (float) getConfig().getDouble("messages.mention.pitch", 1.0);
}
// ── Звуки личных сообщений ──────────────────────────────────────────────────
public boolean isPmSoundEnabled() {
return getConfig().getBoolean("pm.enabled", true);
return getConfig().getBoolean("messages.private-message.enabled", true);
}
public String getPmSound() {
return getConfig().getString("pm.sound", "ENTITY_EXPERIENCE_ORB_PICKUP");
return getConfig().getString("messages.private-message.sound", "ENTITY_EXPERIENCE_ORB_PICKUP");
}
public float getPmVolume() {
return (float) getConfig().getDouble("pm.volume", 1.0);
return (float) getConfig().getDouble("messages.private-message.volume", 1.0);
}
public float getPmPitch() {
return (float) getConfig().getDouble("pm.pitch", 1.0);
return (float) getConfig().getDouble("messages.private-message.pitch", 1.0);
}
// ── Звуки команд ────────────────────────────────────────────────────────────
public boolean isCommandSoundEnabled() {
return getConfig().getBoolean("command.enabled", false);
return getConfig().getBoolean("commands.success.enabled", false);
}
public String getCommandSound() {
return getConfig().getString("command.sound", "UI_BUTTON_CLICK");
return getConfig().getString("commands.success.sound", "UI_BUTTON_CLICK");
}
public float getCommandVolume() {
return (float) getConfig().getDouble("command.volume", 0.5);
return (float) getConfig().getDouble("commands.success.volume", 0.5);
}
public float getCommandPitch() {
return (float) getConfig().getDouble("command.pitch", 1.0);
return (float) getConfig().getDouble("commands.success.pitch", 1.0);
}
}

View file

@ -106,6 +106,7 @@ public class PluginInitializer {
MessagingService messagingService, NickService nickService,
PunishmentService punishmentService) {
AdvancedMessageFilter filter = new AdvancedMessageFilter(plugin.getConfig(), plugin);
plugin.setChatFilter(filter);
plugin.getServer().getPluginManager().registerEvents(
new ChatEventListener(plugin, messageService, playerService, filter), plugin

View file

@ -1,5 +1,7 @@
package com.loki.lochat.core.filter;
import com.loki.lochat.LoChat;
import com.loki.lochat.config.FiltersConfig;
import com.loki.lochat.core.filter.filters.CharacterFilter;
import com.loki.lochat.core.filter.filters.FloodFilter;
import com.loki.lochat.core.filter.filters.SpamFilter;
@ -17,7 +19,7 @@ import java.util.UUID;
*/
public class AdvancedMessageFilter {
private final FileConfiguration config;
private final FiltersConfig filtersConfig;
private final CapsFilter capsFilter;
private final SwearFilter swearFilter;
private final FloodFilter floodFilter;
@ -25,39 +27,32 @@ public class AdvancedMessageFilter {
private final UrlFilter urlFilter;
private final CharacterFilter characterFilter;
public AdvancedMessageFilter(FileConfiguration config) {
this(config, null);
public AdvancedMessageFilter(FileConfiguration config, JavaPlugin plugin) {
this.filtersConfig = resolveFiltersConfig(plugin);
int maxCapsPercent = filtersConfig.getCapsMaxPercent();
int minLength = filtersConfig.getCapsMinLength();
boolean autoLower = filtersConfig.isCapsAutoLowercase();
boolean blockCaps = filtersConfig.isCapsBlock();
this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps);
this.swearFilter = new SwearFilter(filtersConfig, plugin);
this.floodFilter = new FloodFilter(filtersConfig);
this.spamFilter = new SpamFilter(filtersConfig);
this.urlFilter = new UrlFilter(filtersConfig);
this.characterFilter = new CharacterFilter(filtersConfig);
}
public AdvancedMessageFilter(FileConfiguration config, JavaPlugin plugin) {
this.config = config;
// Инициализация CapsFilter
com.loki.lochat.config.FiltersConfig filtersConfig = null;
if (plugin instanceof com.loki.lochat.LoChat loChat) {
filtersConfig = loChat.getConfigManager().getFiltersConfig();
private static FiltersConfig resolveFiltersConfig(JavaPlugin plugin) {
if (plugin instanceof LoChat loChat) {
return loChat.getConfigManager().getFiltersConfig();
}
if (filtersConfig != null) {
int maxCapsPercent = filtersConfig.getCapsMaxPercent();
int minLength = filtersConfig.getCapsMinLength();
boolean autoLower = filtersConfig.isCapsAutoLowercase();
boolean blockCaps = filtersConfig.isCapsBlock();
this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps);
} else {
int maxCapsPercent = config.getInt("filters.caps.max-percent", 70);
int minLength = config.getInt("filters.caps.min-length", 5);
boolean autoLower = config.getBoolean("filters.caps.auto-lowercase", true);
boolean blockCaps = config.getBoolean("filters.caps.block", false);
this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps);
if (plugin != null) {
FiltersConfig filtersConfig = new FiltersConfig(plugin);
filtersConfig.init();
return filtersConfig;
}
// Инициализация остальных фильтров
this.swearFilter = new SwearFilter(config, plugin);
this.floodFilter = new FloodFilter(config);
this.spamFilter = new SpamFilter(config);
this.urlFilter = new UrlFilter(config);
this.characterFilter = new CharacterFilter(config);
throw new IllegalArgumentException("AdvancedMessageFilter requires a FiltersConfig");
}
/**
@ -74,7 +69,7 @@ public class AdvancedMessageFilter {
public FilterResult filterMessage(Player player, String message) {
// 1. Капс
if (config.getBoolean("filters.caps.enabled", true)) {
if (filtersConfig.isCapsEnabled()) {
String filtered = capsFilter.filter(player, message);
if (filtered == null) {
return FilterResult.blocked("&#CF6679Не кричите в чате");
@ -83,7 +78,7 @@ public class AdvancedMessageFilter {
}
// 2. Мат
if (config.getBoolean("filters.swear.enabled", true) && !canBypassFilter(player, "swear")) {
if (filtersConfig.isSwearEnabled() && !canBypassFilter(player, "swear")) {
FilterResult swear = swearFilter.filter(player, message);
if (!swear.allowed()) {
return swear;
@ -92,7 +87,7 @@ public class AdvancedMessageFilter {
}
// 3. URL фильтры
if (config.getBoolean("filters.advertising.enabled", true)) {
if (filtersConfig.isAdvertisingEnabled()) {
FilterResult r = urlFilter.filterUrls(player, message);
if (!r.allowed()) {
return r;
@ -106,7 +101,7 @@ public class AdvancedMessageFilter {
}
// 4. IP
if (config.getBoolean("filters.ip.enabled", true)) {
if (filtersConfig.isIpEnabled()) {
FilterResult r = urlFilter.filterIPs(player, message);
if (!r.allowed()) {
return r;
@ -115,12 +110,12 @@ public class AdvancedMessageFilter {
}
// 5. Повторяющиеся символы
if (config.getBoolean("filters.repeat.enabled", true)) {
if (filtersConfig.isRepeatEnabled()) {
message = characterFilter.filter(message);
}
// 6. Anti-flood
if (config.getBoolean("filters.flood.enabled", true) && !canBypassFilter(player, "flood")) {
if (filtersConfig.isFloodEnabled() && !canBypassFilter(player, "flood")) {
FilterResult r = floodFilter.filter(player);
if (!r.allowed()) {
return r;
@ -128,7 +123,7 @@ public class AdvancedMessageFilter {
}
// 7. Anti-spam
if (config.getBoolean("filters.spam.enabled", true) && !canBypassFilter(player, "spam")) {
if (filtersConfig.isSpamEnabled() && !canBypassFilter(player, "spam")) {
FilterResult r = spamFilter.filter(player, message);
if (!r.allowed()) {
return r;

View file

@ -1,6 +1,6 @@
package com.loki.lochat.core.filter.filters;
import org.bukkit.configuration.file.FileConfiguration;
import com.loki.lochat.config.FiltersConfig;
import java.util.regex.Pattern;
@ -8,9 +8,13 @@ public class CharacterFilter {
private final boolean enabled;
private final int maxRepeating;
public CharacterFilter(FileConfiguration config) {
this.enabled = config.getBoolean("filters.repeating-chars.enabled", true);
this.maxRepeating = config.getInt("filters.repeating-chars.max", 3);
public CharacterFilter(FiltersConfig filters) {
this(filters.isRepeatEnabled(), filters.getRepeatMaxRepeats());
}
public CharacterFilter(boolean enabled, int maxRepeating) {
this.enabled = enabled;
this.maxRepeating = maxRepeating;
}
public String filter(String message) {

View file

@ -1,8 +1,8 @@
package com.loki.lochat.core.filter.filters;
import com.loki.lochat.config.FiltersConfig;
import com.loki.lochat.core.filter.FilterResult;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import java.util.ArrayDeque;
@ -12,11 +12,15 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class FloodFilter {
private final FileConfiguration config;
private static final String FLOOD_BLOCK_MESSAGE = "&#CF6679Не флудите!";
private final int maxMessages;
private final int timePeriod;
private final Map<UUID, Deque<Long>> floodTracker = new ConcurrentHashMap<>();
public FloodFilter(FileConfiguration config) {
this.config = config;
public FloodFilter(FiltersConfig filters) {
this.maxMessages = filters.getFloodMaxMessages();
this.timePeriod = filters.getFloodTimePeriod();
}
public FilterResult filter(Player player) {
@ -24,8 +28,6 @@ public class FloodFilter {
return FilterResult.ok(null);
}
int maxMessages = config.getInt("filters.flood.max-messages", 5);
int timePeriod = config.getInt("filters.flood.time-period", 10);
long now = System.currentTimeMillis();
long windowMs = timePeriod * 1000L;
@ -38,8 +40,7 @@ public class FloodFilter {
}
if (timestamps.size() >= maxMessages) {
return FilterResult.blocked(config.getString("filters.flood.block-message",
"&#CF6679Не флудите!"));
return FilterResult.blocked(FLOOD_BLOCK_MESSAGE);
}
timestamps.addLast(now);

View file

@ -1,8 +1,8 @@
package com.loki.lochat.core.filter.filters;
import com.loki.lochat.config.FiltersConfig;
import com.loki.lochat.core.filter.FilterResult;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import java.util.ArrayDeque;
@ -12,11 +12,20 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class SpamFilter {
private final FileConfiguration config;
private final int maxSimilar;
private final int threshold;
private final String blockMessage;
private final Map<UUID, Deque<String>> spamTracker = new ConcurrentHashMap<>();
public SpamFilter(FileConfiguration config) {
this.config = config;
public SpamFilter(FiltersConfig filters) {
this(filters.getSpamMaxSimilarMessages(), filters.getSpamSimilarityThreshold(),
filters.getSpamBlockMessage());
}
public SpamFilter(int maxSimilar, int threshold, String blockMessage) {
this.maxSimilar = maxSimilar;
this.threshold = threshold;
this.blockMessage = blockMessage;
}
public FilterResult filter(Player player, String message) {
@ -24,9 +33,6 @@ public class SpamFilter {
return FilterResult.ok(message);
}
int maxSimilar = config.getInt("filters.spam.max-similar-messages", 3);
int threshold = config.getInt("filters.spam.similarity-threshold", 80);
Deque<String> history = spamTracker.computeIfAbsent(
player.getUniqueId(), k -> new ArrayDeque<>());
@ -36,8 +42,7 @@ public class SpamFilter {
.count();
if (similarCount >= maxSimilar) {
return FilterResult.blocked(config.getString("filters.spam.block-message",
"&#CF6679Не отправляйте одинаковые сообщения"));
return FilterResult.blocked(blockMessage);
}
// Добавляем в историю (храним последние 10)

View file

@ -1,9 +1,9 @@
package com.loki.lochat.core.filter.filters;
import com.loki.lochat.config.FiltersConfig;
import com.loki.lochat.core.filter.FilterResult;
import com.loki.lochat.utils.text.AhoCorasick;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
@ -20,10 +20,10 @@ public class SwearFilter {
private final String replacement;
private AhoCorasick ahoCorasick;
public SwearFilter(FileConfiguration config, JavaPlugin plugin) {
this.enabled = config.getBoolean("filters.swear.enabled", true);
this.replaceMode = config.getBoolean("filters.swear.replace", false);
this.replacement = config.getString("filters.swear.replacement", "***");
public SwearFilter(FiltersConfig filters, JavaPlugin plugin) {
this.enabled = filters.isSwearEnabled();
this.replaceMode = "replace".equals(filters.getSwearAction());
this.replacement = filters.getSwearReplacementChar();
if (enabled) {
loadWordList(plugin);

View file

@ -1,8 +1,8 @@
package com.loki.lochat.core.filter.filters;
import com.loki.lochat.config.FiltersConfig;
import com.loki.lochat.core.filter.FilterResult;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import java.util.HashSet;
@ -23,14 +23,23 @@ public class UrlFilter {
"(?i)(h\\s*t\\s*t\\s*p|w\\s*w\\s*w|d\\s*o\\s*t\\s*c\\s*o\\s*m)"
);
private final FileConfiguration config;
private static final String URL_BLOCKED_MESSAGE = "&#CF6679Ссылки запрещены!";
private static final String URL_BLACKLISTED_MESSAGE = "&#CF6679Эта ссылка запрещена!";
private static final String IP_BLOCKED_MESSAGE = "&#CF6679IP адреса запрещены!";
private static final String HIDDEN_URL_BLOCKED_MESSAGE = "&#CF6679Реклама запрещена!";
private final boolean blockHiddenUrls;
private final boolean blockIp;
private final String ipReplacement;
private final Set<String> whitelistedDomains;
private final Set<String> blacklistedDomains;
public UrlFilter(FileConfiguration config) {
this.config = config;
this.whitelistedDomains = new HashSet<>(config.getStringList("filters.advertising.whitelist"));
this.blacklistedDomains = new HashSet<>(config.getStringList("filters.advertising.blacklist"));
public UrlFilter(FiltersConfig filters) {
this.blockHiddenUrls = filters.isBlockHiddenUrls();
this.blockIp = filters.isIpBlock();
this.ipReplacement = filters.getIpReplacement();
this.whitelistedDomains = new HashSet<>(filters.getWhitelistedDomains());
this.blacklistedDomains = new HashSet<>(filters.getBlacklistedDomains());
}
public FilterResult filterUrls(Player player, String message) {
@ -46,8 +55,7 @@ public class UrlFilter {
boolean ok = whitelistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (!ok) {
return FilterResult.blocked(config.getString("filters.url.blocked-message",
"&#CF6679Ссылки запрещены!"));
return FilterResult.blocked(URL_BLOCKED_MESSAGE);
}
}
@ -55,8 +63,7 @@ public class UrlFilter {
boolean bad = blacklistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (bad) {
return FilterResult.blocked(config.getString("filters.url.blocked-message",
"&#CF6679Эта ссылка запрещена!"));
return FilterResult.blocked(URL_BLACKLISTED_MESSAGE);
}
}
}
@ -71,19 +78,17 @@ public class UrlFilter {
Matcher matcher = IP_PATTERN.matcher(message);
if (matcher.find()) {
if (config.getBoolean("filters.ip.block", false)) {
return FilterResult.blocked(config.getString("filters.ip.blocked-message",
"&#CF6679IP адреса запрещены!"));
if (blockIp) {
return FilterResult.blocked(IP_BLOCKED_MESSAGE);
}
return FilterResult.ok(matcher.replaceAll(
config.getString("filters.ip.replacement", "[IP скрыт]")));
return FilterResult.ok(matcher.replaceAll(ipReplacement));
}
return FilterResult.ok(message);
}
public FilterResult filterHiddenUrls(Player player, String message) {
if (!config.getBoolean("filters.advertising.block-hidden-urls", true)) {
if (!blockHiddenUrls) {
return FilterResult.ok(message);
}
if (player.hasPermission("lochat.bypass.hiddenurls")) {
@ -91,8 +96,7 @@ public class UrlFilter {
}
if (HIDDEN_URL_PATTERN.matcher(message).find()) {
return FilterResult.blocked(config.getString("filters.advertising.blocked-message",
"&#CF6679Реклама запрещена!"));
return FilterResult.blocked(HIDDEN_URL_BLOCKED_MESSAGE);
}
return FilterResult.ok(message);

View file

@ -47,8 +47,10 @@ public class ServiceRegistry {
PlayerService playerService = new PlayerServiceImpl(plugin);
register(PlayerService.class, playerService);
register(ChatService.class, new ChatServiceImpl(plugin));
register(MessageService.class, new MessageServiceImpl(plugin, muteService, playerService));
MessageService messageService = new MessageServiceImpl(plugin, muteService, playerService);
register(MessageService.class, messageService);
register(ChatService.class, new ChatServiceImpl(plugin, messageService));
PrivateMessageServiceImpl pmService = new PrivateMessageServiceImpl((LoChat) plugin);
MessagingService messagingService = ServiceFactory.createMessagingService(plugin, messageConfig, pmService);

View file

@ -1,6 +1,11 @@
package com.loki.lochat.core.service;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChatService;
import com.loki.lochat.api.service.MessageService;
import com.loki.lochat.config.AppearanceConfig;
import com.loki.lochat.core.filter.AdvancedMessageFilter;
import com.loki.lochat.core.filter.FilterResult;
import com.loki.lochat.renderer.EnhancedChatRenderer;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.persistence.FilePersistence;
@ -8,6 +13,7 @@ import com.loki.lochat.utils.platform.FoliaUtil;
import com.loki.lochat.utils.player.DistanceUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.FileConfiguration;
@ -26,10 +32,12 @@ import java.util.concurrent.ConcurrentHashMap;
public class ChatServiceImpl implements ChatService {
private final JavaPlugin plugin;
private final MessageService messageService;
private final Set<UUID> globalChatDisabled = ConcurrentHashMap.newKeySet();
public ChatServiceImpl(JavaPlugin plugin) {
public ChatServiceImpl(JavaPlugin plugin, MessageService messageService) {
this.plugin = plugin;
this.messageService = messageService;
loadDisabled();
}
@ -39,7 +47,10 @@ public class ChatServiceImpl implements ChatService {
@Override
public void sendGlobalMessage(Player sender, Object message) {
Component msg = toComponent(message);
Component msg = toComponent(sender, message, true);
if (msg == null) {
return;
}
EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, true);
for (Player p : Bukkit.getOnlinePlayers()) {
if (!isGlobalChatDisabled(p.getUniqueId())) {
@ -50,8 +61,11 @@ public class ChatServiceImpl implements ChatService {
@Override
public void sendLocalMessage(Player sender, Object message) {
int radius = plugin.getConfig().getInt("chat.local.radius", 100);
Component msg = toComponent(message);
int radius = ((LoChat) plugin).getConfigManager().getAppearanceConfig().getLocalRadius();
Component msg = toComponent(sender, message, false);
if (msg == null) {
return;
}
EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, false);
int count = 0;
@ -112,7 +126,38 @@ public class ChatServiceImpl implements ChatService {
FilePersistence.saveYaml(plugin, "data/players.yml", cfg);
}
private Component toComponent(Object message) {
return message instanceof Component c ? c : ChatFormatter.parse(message.toString());
private Component toComponent(Player sender, Object message, boolean isGlobal) {
if (!(message instanceof String text)) {
return message instanceof Component c ? c : ChatFormatter.parse(message.toString());
}
AppearanceConfig appearance = ((LoChat) plugin).getConfigManager().getAppearanceConfig();
int minLength = appearance.getMinMessageLength();
int maxLength = appearance.getMaxMessageLength();
if (text.length() < minLength || text.length() > maxLength) {
sender.sendMessage(ChatFormatter.parse(
"&#CF6679Сообщение должно содержать от " + minLength + " до " + maxLength + " символов"));
return null;
}
if (!messageService.processMessage(sender, isGlobal ? "!" + text : text)) {
return null;
}
AdvancedMessageFilter filter = ((LoChat) plugin).getChatFilter();
if (filter != null) {
FilterResult result = filter.filterMessage(sender, text);
if (!result.allowed()) {
sender.sendMessage(ChatFormatter.parse(result.blockReason()));
return null;
}
text = result.filteredMessage();
}
String safe = MiniMessage.miniMessage().escapeTags(text);
if (sender.hasPermission("lochat.chat.colors")) {
return ChatFormatter.parse(safe);
}
return Component.text(safe);
}
}

View file

@ -32,6 +32,7 @@ public class PlayerServiceImpl implements PlayerService {
private final AtomicLong totalMessages = new AtomicLong(0);
private FileConfiguration stats;
private final Object statsLock = new Object();
public PlayerServiceImpl(JavaPlugin plugin) {
this.plugin = plugin;
@ -113,26 +114,30 @@ public class PlayerServiceImpl implements PlayerService {
@Override
public void saveAll() {
stats.set("global.total-messages",
stats.getLong("global.total-messages", 0) + totalMessages.getAndSet(0));
stats.set("by-type.global-chat",
stats.getLong("by-type.global-chat", 0) + globalChatCount.getAndSet(0));
stats.set("by-type.local-chat",
stats.getLong("by-type.local-chat", 0) + localChatCount.getAndSet(0));
stats.set("by-type.private-messages",
stats.getLong("by-type.private-messages", 0) + pmCount.getAndSet(0));
stats.set("last-updated", System.currentTimeMillis());
FilePersistence.saveYaml(plugin, "data/statistics.yml", stats);
synchronized (statsLock) {
stats.set("global.total-messages",
stats.getLong("global.total-messages", 0) + totalMessages.getAndSet(0));
stats.set("by-type.global-chat",
stats.getLong("by-type.global-chat", 0) + globalChatCount.getAndSet(0));
stats.set("by-type.local-chat",
stats.getLong("by-type.local-chat", 0) + localChatCount.getAndSet(0));
stats.set("by-type.private-messages",
stats.getLong("by-type.private-messages", 0) + pmCount.getAndSet(0));
stats.set("last-updated", System.currentTimeMillis());
FilePersistence.saveYaml(plugin, "data/statistics.yml", stats);
}
}
// ========== Private Methods ==========
private void persistPlayerMessages(UUID uuid, long count) {
String path = "top-players.by-messages." + uuid;
long prev = stats.getLong(path + ".count", 0);
stats.set(path + ".count", prev + count);
stats.set(path + ".last-updated", System.currentTimeMillis());
FilePersistence.saveYaml(plugin, "data/statistics.yml", stats);
synchronized (statsLock) {
String path = "top-players.by-messages." + uuid;
long prev = stats.getLong(path + ".count", 0);
stats.set(path + ".count", prev + count);
stats.set(path + ".last-updated", System.currentTimeMillis());
FilePersistence.saveYaml(plugin, "data/statistics.yml", stats);
}
playerMessages.remove(uuid);
}

View file

@ -94,7 +94,6 @@ public class PunishmentServiceImpl implements PunishmentService {
}
if (b.isExpired()) {
bans.remove(uuid, b);
saveAsync();
return false;
}
return true;

View file

@ -4,16 +4,15 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.loki.lochat.data.model.MuteData;
import com.loki.lochat.utils.persistence.FilePersistence;
import com.loki.lochat.utils.platform.FoliaUtil;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
@ -87,27 +86,12 @@ public class MuteDataStorage {
}
private void save() {
try {
ensureDataFolderExists();
Map<String, MuteData> toSave = new ConcurrentHashMap<>();
mutes.forEach((uuid, data) -> toSave.put(uuid.toString(), data));
try (Writer writer = new FileWriter(dataFile)) {
gson.toJson(toSave, writer);
}
} catch (IOException e) {
plugin.getLogger().warning("Error saving mutes: " + e.getMessage());
}
Map<String, MuteData> toSave = new HashMap<>();
mutes.forEach((uuid, data) -> toSave.put(uuid.toString(), data));
FilePersistence.saveJson(plugin, "mutes.json", toSave, gson);
}
private void saveAsync() {
FoliaUtil.runAsync(plugin, this::save);
}
private void ensureDataFolderExists() {
if (!dataFile.getParentFile().exists()) {
dataFile.getParentFile().mkdirs();
}
}
}

View file

@ -7,19 +7,28 @@ import com.loki.lochat.config.RatConfig;
import com.loki.lochat.data.model.MuteData;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
public class MuteHistoryManager {
private static final ConcurrentHashMap<String, ReentrantLock> FILE_LOCKS = new ConcurrentHashMap<>();
private final Map<UUID, List<MuteData.MuteHistoryEntry>> history = new ConcurrentHashMap<>();
private final File historyFile;
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
@ -86,10 +95,35 @@ public class MuteHistoryManager {
}
public void save() {
try (FileWriter writer = new FileWriter(historyFile)) {
gson.toJson(history, writer);
ReentrantLock lock = FILE_LOCKS.computeIfAbsent(
historyFile.getAbsolutePath(), k -> new ReentrantLock());
lock.lock();
try {
File parent = historyFile.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
File tmp = parent != null
? new File(parent, historyFile.getName() + ".tmp")
: new File(historyFile.getName() + ".tmp");
try (Writer writer = new OutputStreamWriter(
new FileOutputStream(tmp), StandardCharsets.UTF_8)) {
gson.toJson(history, writer);
}
moveIntoPlace(tmp);
} catch (IOException e) {
e.printStackTrace();
System.err.println("Failed to save mute history: " + e.getMessage());
} finally {
lock.unlock();
}
}
private void moveIntoPlace(File tmp) throws IOException {
try {
Files.move(tmp.toPath(), historyFile.toPath(),
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tmp.toPath(), historyFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}

View file

@ -12,11 +12,9 @@ public class GradientConfig {
private final JavaPlugin plugin;
private FileConfiguration config;
private FileConfiguration mainConfig;
public GradientConfig(JavaPlugin plugin) {
this.plugin = plugin;
this.mainConfig = plugin.getConfig();
loadConfig();
}
@ -28,7 +26,6 @@ public class GradientConfig {
public void reload() {
plugin.reloadConfig();
this.mainConfig = plugin.getConfig();
this.config = plugin.getConfig();
}
@ -42,56 +39,51 @@ public class GradientConfig {
}
public int getMaxColors() {
return config.getInt("gradient.max-colors", 7);
return config.getInt("gradient.limits.max-colors", 7);
}
public int getMinColors() {
return config.getInt("gradient.min-colors", 1);
return config.getInt("gradient.limits.min-colors", 1);
}
public int getPricePerColor() {
return config.getInt("gradient.price-per-color", 50);
return config.getInt("gradient.pricing.price-per-color", 50);
}
public int getPrefixPrice() {
return config.getInt("gradient.prefix-price", 500);
return config.getInt("gradient.pricing.prefix-price", 500);
}
public boolean isPrefixOneTimePurchase() {
return config.getBoolean("gradient.prefix-one-time-purchase", true);
return config.getBoolean("gradient.pricing.prefix-one-time-purchase", true);
}
public int getColorCooldown() {
return config.getInt("gradient.color-cooldown", 60);
return config.getInt("gradient.cooldowns.color", 60);
}
public int getPrefixCooldown() {
return config.getInt("gradient.prefix-cooldown", 120);
return config.getInt("gradient.cooldowns.prefix", 120);
}
public int getMaxPrefixLength() {
return config.getInt("gradient.max-prefix-length", 16);
return config.getInt("gradient.limits.max-prefix-length", 16);
}
public String getPrefixFormat() {
// Сначала проверяем в formatting.prefix-format (из gradient-config.yml)
if (config.contains("formatting.prefix-format")) {
return config.getString("formatting.prefix-format", "[{prefix}] ");
}
// Затем проверяем в gradient.prefix-format (из config.yml)
return config.getString("gradient.prefix-format", "[{prefix}] ");
return config.getString("gradient.formatting.prefix-format", "[{prefix}] ");
}
public boolean isGradientOnPrefix() {
return config.getBoolean("gradient.gradient-on-prefix", true);
return config.getBoolean("gradient.formatting.gradient-on-prefix", true);
}
public boolean isGradientOnLuckPermsPrefix() {
return config.getBoolean("gradient.gradient-on-luckperms-prefix", true);
return config.getBoolean("gradient.formatting.gradient-on-luckperms-prefix", true);
}
public boolean isContinuousGradient() {
return config.getBoolean("gradient.continuous-gradient", true);
return config.getBoolean("gradient.formatting.continuous-gradient", true);
}
public boolean isUseLegacyRgbFormat() {
@ -99,15 +91,15 @@ public class GradientConfig {
}
public String getStorageType() {
return config.getString("gradient.storage-type", "file");
return config.getString("gradient.storage.type", "file");
}
public String getSqliteFile() {
return config.getString("gradient.sqlite-file", "gradient-data.db");
return config.getString("gradient.storage.sqlite-file", "gradient-data.db");
}
public List<String> getPrefixBlacklist() {
return config.getStringList("gradient.prefix-blacklist");
return List.of();
}
public boolean isPrefixBlacklisted(String prefix) {
@ -117,16 +109,10 @@ public class GradientConfig {
}
public boolean isUpdateDisplayName() {
if (mainConfig.contains("display.update-display-name")) {
return mainConfig.getBoolean("display.update-display-name", true);
}
return config.getBoolean("display.update-display-name", true);
return config.getBoolean("gradient.display.update-display-name", true);
}
public boolean isUpdateTabList() {
if (mainConfig.contains("display.update-tab-list")) {
return mainConfig.getBoolean("display.update-tab-list", true);
}
return config.getBoolean("display.update-tab-list", true);
return config.getBoolean("gradient.display.update-tab-list", true);
}
}

View file

@ -52,7 +52,7 @@ public class DiscordConfig {
}
public String getAvatarUrl() {
return config.getString("webhook.avatar_url", "");
return config.getString("webhook.avatar-url", "");
}
public int getTimeout() {
@ -60,11 +60,11 @@ public class DiscordConfig {
}
public int getRetryAttempts() {
return config.getInt("performance.retry_attempts", 3);
return config.getInt("performance.retry-attempts", 3);
}
public long getRetryDelay() {
return config.getLong("performance.retry_delay", 1000);
return config.getLong("performance.retry-delay", 1000);
}
public boolean isAsync() {
@ -84,11 +84,11 @@ public class DiscordConfig {
}
public boolean isGlobalOnly() {
return config.getBoolean("chat.global_only", true);
return config.getBoolean("chat.global-only", true);
}
public boolean useEmbed() {
return config.getBoolean("chat.use_embed", true);
return config.getBoolean("chat.use-embed", true);
}
public String getChatFormat() {
@ -96,7 +96,7 @@ public class DiscordConfig {
}
public String getEmbedColor() {
return config.getString("chat.embed_color", "5865F2");
return config.getString("chat.embed-color", "5865F2");
}
public boolean isEventEnabled(String event) {
@ -121,14 +121,14 @@ public class DiscordConfig {
}
public int getMinMessageLength() {
return config.getInt("filter.min_message_length", 1);
return config.getInt("filter.min-message-length", 1);
}
public int getMaxMessageLength() {
return config.getInt("filter.max_message_length", 2000);
return config.getInt("filter.max-message-length", 2000);
}
public boolean sanitizeMentions() {
return config.getBoolean("filter.sanitize_mentions", true);
return config.getBoolean("filter.sanitize-mentions", true);
}
}

View file

@ -12,8 +12,6 @@ import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import io.papermc.paper.event.player.AsyncChatEvent;
/**
* Слушатель событий для Discord интеграции
*/
@ -24,29 +22,6 @@ public class DiscordEventListener implements Listener {
this.discord = discord;
}
/**
* Обработка сообщений чата для Discord
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onChat(AsyncChatEvent event) {
if (!discord.isEnabled()) {
return;
}
Player player = event.getPlayer();
String message = PlainTextComponentSerializer.plainText().serialize(event.message());
// Определяем тип чата (глобальный если начинается с !)
boolean isGlobal = message.startsWith("!");
// Очищаем сообщение от ! если это глобальный чат
if (isGlobal) {
message = message.substring(1).stripLeading();
}
discord.sendChatMessage(player, message, isGlobal);
}
/**
* Обработка входа игрока
*/

View file

@ -44,11 +44,11 @@ public class AutoMessageManager {
// Настройки
prefix = plugin.getConfigManager().getString("prefix", "&#00D4FF[LoChat]");
mode = "sequential"; // Всегда последовательно
mode = config.getBoolean("automessages.random", false) ? "random" : "sequential";
// Загружаем сообщения из config.yml
messages = new LinkedHashMap<>();
ConfigurationSection msgSection = config.getConfigurationSection("automessages-list");
ConfigurationSection msgSection = config.getConfigurationSection("automessages.messages");
if (msgSection != null) {
for (String key : msgSection.getKeys(false)) {

View file

@ -55,7 +55,12 @@ public class EnhancedChatRenderer implements ChatRenderer {
if (viewer instanceof Player vp) {
processed = mentionHandler.processMentions(processed, source, vp);
processed = applyTranslation(processed, vp);
if (cfg.isTranslationEnabled()) {
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
if (loChat.getTranslationService().isEnabled()) {
sendTranslationAsync(vp, PlainTextComponentSerializer.plainText().serialize(processed));
}
}
}
// Получаем формат из конфига
@ -202,31 +207,26 @@ public class EnhancedChatRenderer implements ChatRenderer {
.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
}
private Component applyTranslation(Component message, Player viewer) {
if (!cfg.isTranslationEnabled()) {
return message;
private void sendTranslationAsync(Player viewer, String plainText) {
if (plainText.isBlank()) {
return;
}
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
TranslationService ts = loChat.getTranslationService();
if (!ts.isEnabled()) {
return message;
}
String plainText = PlainTextComponentSerializer.plainText().serialize(message);
if (plainText.isBlank()) {
return message;
}
String viewerLang = viewer.locale().getLanguage();
String translated = ts.translate(plainText, viewerLang);
if (translated.equals(plainText)) {
return message;
}
String langName = LANGUAGE_NAMES.getOrDefault(viewerLang, viewerLang.toUpperCase(java.util.Locale.ROOT));
Component hover = Component.text()
.append(com.loki.lochat.utils.format.ChatFormatter.parse(
"&#AAAAAA[" + langName + "]&#FFFFFF "))
.append(Component.text(translated))
.build();
return message.hoverEvent(HoverEvent.showText(hover));
ts.translateAsync(plainText, viewerLang)
.thenAccept(translated -> {
if (translated.equals(plainText)) {
return;
}
String langName = LANGUAGE_NAMES.getOrDefault(viewerLang, viewerLang.toUpperCase(java.util.Locale.ROOT));
Component component = Component.text()
.append(com.loki.lochat.utils.format.ChatFormatter.parse(
"&#AAAAAA[" + langName + "]&#FFFFFF "))
.append(Component.text(translated))
.build();
viewer.sendActionBar(component);
});
}
private static final Map<String, String> LANGUAGE_NAMES = new HashMap<>();

View file

@ -10,19 +10,26 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Logger;
public class TranslationService {
private static final Duration TIMEOUT = Duration.ofSeconds(10);
private static final int CACHE_MAX_SIZE = 2000;
private final Logger logger;
private final HttpClient httpClient;
private final Gson gson;
private final Map<String, Map<String, String>> cache = new ConcurrentHashMap<>();
private final Map<String, Map<String, String>> cache;
private final ExecutorService executor;
private final String endpoint;
private final String apiKey;
private final boolean enabled;
@ -36,39 +43,53 @@ public class TranslationService {
.connectTimeout(TIMEOUT)
.build();
this.gson = new Gson();
this.cache = Collections.synchronizedMap(new LinkedHashMap<String, Map<String, String>>() {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Map<String, String>> eldest) {
return size() > CACHE_MAX_SIZE;
}
});
this.executor = Executors.newFixedThreadPool(2, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "lochat-translation");
t.setDaemon(true);
return t;
}
});
}
public boolean isEnabled() {
return enabled && !endpoint.isBlank();
}
public String translate(String text, String targetLanguage) {
public CompletableFuture<String> translateAsync(String text, String targetLanguage) {
if (!isEnabled() || text == null || text.isBlank()) {
return text;
return CompletableFuture.completedFuture(text);
}
String langCode = localeToLanguageCode(targetLanguage);
String cacheKey = text.intern();
Map<String, String> langMap = cache.get(cacheKey);
if (langMap != null) {
String cached = langMap.get(langCode);
if (cached != null) {
return cached;
}
String cached = getCached(text, langCode);
if (cached != null) {
return CompletableFuture.completedFuture(cached);
}
return CompletableFuture.supplyAsync(() -> fetchTranslation(text, langCode), executor);
}
private String fetchTranslation(String text, String targetLang) {
try {
String translation = fetchTranslation(text, langCode);
cache.computeIfAbsent(cacheKey, k -> new ConcurrentHashMap<>()).put(langCode, translation);
String translation = doFetch(text, targetLang);
putCached(text, targetLang, translation);
return translation;
} catch (Exception e) {
logger.warning("Translation failed for \"" + abbreviate(text) + "\" to " + langCode + ": " + e.getMessage());
logger.warning("Translation failed for \"" + abbreviate(text) + "\" to " + targetLang + ": " + e.getMessage());
return text;
}
}
private String fetchTranslation(String text, String targetLang) throws Exception {
private String doFetch(String text, String targetLang) throws Exception {
JsonObject body = new JsonObject();
body.addProperty("q", text);
body.addProperty("source", "auto");
@ -95,6 +116,27 @@ public class TranslationService {
return json.get("translatedText").getAsString();
}
private String getCached(String text, String langCode) {
synchronized (cache) {
Map<String, String> langMap = cache.get(text);
if (langMap != null) {
return langMap.get(langCode);
}
}
return null;
}
private void putCached(String text, String langCode, String translation) {
synchronized (cache) {
Map<String, String> langMap = cache.get(text);
if (langMap == null) {
langMap = new LinkedHashMap<>();
cache.put(text, langMap);
}
langMap.put(langCode, translation);
}
}
public static String localeToLanguageCode(String locale) {
if (locale == null || locale.isBlank()) {
return "en";

View file

@ -15,9 +15,16 @@ import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
public final class FilePersistence {
private static final ConcurrentHashMap<String, ReentrantLock> FILE_LOCKS = new ConcurrentHashMap<>();
private FilePersistence() {}
public static File getFile(JavaPlugin plugin, String name) {
@ -34,12 +41,18 @@ public final class FilePersistence {
}
public static void saveYaml(JavaPlugin plugin, String name, FileConfiguration config) {
File target = getFile(plugin, name);
ReentrantLock lock = lockFor(target);
lock.lock();
try {
File file = getFile(plugin, name);
file.getParentFile().mkdirs();
config.save(file);
target.getParentFile().mkdirs();
File tmp = new File(target.getParentFile(), target.getName() + ".tmp");
config.save(tmp);
moveIntoPlace(tmp, target);
} catch (IOException e) {
plugin.getLogger().warning("Failed to save " + name + ": " + e.getMessage());
} finally {
lock.unlock();
}
}
@ -57,14 +70,33 @@ public final class FilePersistence {
}
public static void saveJson(JavaPlugin plugin, String name, Object data, Gson gson) {
File target = getFile(plugin, name);
ReentrantLock lock = lockFor(target);
lock.lock();
try {
File file = getFile(plugin, name);
file.getParentFile().mkdirs();
try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) {
target.getParentFile().mkdirs();
File tmp = new File(target.getParentFile(), target.getName() + ".tmp");
try (Writer writer = new OutputStreamWriter(new FileOutputStream(tmp), StandardCharsets.UTF_8)) {
gson.toJson(data, writer);
}
moveIntoPlace(tmp, target);
} catch (IOException e) {
plugin.getLogger().warning("Failed to save " + name + ": " + e.getMessage());
} finally {
lock.unlock();
}
}
private static ReentrantLock lockFor(File target) {
return FILE_LOCKS.computeIfAbsent(target.getAbsolutePath(), k -> new ReentrantLock());
}
private static void moveIntoPlace(File tmp, File target) throws IOException {
try {
Files.move(tmp.toPath(), target.toPath(),
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tmp.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}
}

View file

@ -218,3 +218,15 @@ messages:
hub-command:
enabled: true
server: "hub"
# ── Auto Messages ──────────────────────────────────────────────────────────────
automessages:
enabled: true
interval: 300
random: false
messages:
welcome:
- "&7Добро пожаловать на сервер! &b%online%&7 игроков онлайн"
vote:
- "&7Голосуй за сервер и получай награды!"
- "&7Ссылка: example.com/vote"

View file

@ -2,78 +2,57 @@ package com.loki.lochat.core.filter;
import com.loki.lochat.core.filter.filters.CharacterFilter;
import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CharacterFilterTest {
@Test
void filter_limitsRepeatingChars() {
YamlConfiguration config = new YamlConfiguration();
config.set("filters.repeating-chars.enabled", true);
config.set("filters.repeating-chars.max", 3);
CharacterFilter filter = new CharacterFilter(config);
CharacterFilter filter = new CharacterFilter(true, 3);
assertEquals("привееет", filter.filter("привеееееет"));
}
@Test
void filter_noChangeWhenUnderLimit() {
YamlConfiguration config = new YamlConfiguration();
config.set("filters.repeating-chars.enabled", true);
config.set("filters.repeating-chars.max", 3);
CharacterFilter filter = new CharacterFilter(config);
CharacterFilter filter = new CharacterFilter(true, 3);
assertEquals("привет", filter.filter("привет"));
}
@Test
void filter_shortRepeatsAllowed() {
YamlConfiguration config = new YamlConfiguration();
config.set("filters.repeating-chars.enabled", true);
config.set("filters.repeating-chars.max", 3);
CharacterFilter filter = new CharacterFilter(config);
CharacterFilter filter = new CharacterFilter(true, 3);
assertEquals("ууупс", filter.filter("ууупс"));
}
@Test
void filter_disabledReturnsOriginal() {
YamlConfiguration config = new YamlConfiguration();
config.set("filters.repeating-chars.enabled", false);
CharacterFilter filter = new CharacterFilter(config);
CharacterFilter filter = new CharacterFilter(false, 3);
assertEquals("мноооого букв", filter.filter("мноооого букв"));
}
@Test
void filter_zeroMaxDisabled() {
YamlConfiguration config = new YamlConfiguration();
config.set("filters.repeating-chars.enabled", true);
config.set("filters.repeating-chars.max", 0);
CharacterFilter filter = new CharacterFilter(config);
CharacterFilter filter = new CharacterFilter(true, 0);
assertEquals("теееекст", filter.filter("теееекст"));
}
@Test
void filter_defaultConfig() {
CharacterFilter filter = new CharacterFilter(new YamlConfiguration());
CharacterFilter filter = new CharacterFilter(true, 3);
assertEquals("тееест", filter.filter("теееест"));
}
@Test
void filter_differentMax() {
YamlConfiguration config = new YamlConfiguration();
config.set("filters.repeating-chars.enabled", true);
config.set("filters.repeating-chars.max", 1);
CharacterFilter filter = new CharacterFilter(config);
CharacterFilter filter = new CharacterFilter(true, 1);
assertEquals("ап", filter.filter("ааап"));
}
@Test
void filter_multiplePatterns() {
YamlConfiguration config = new YamlConfiguration();
config.set("filters.repeating-chars.enabled", true);
config.set("filters.repeating-chars.max", 2);
CharacterFilter filter = new CharacterFilter(config);
CharacterFilter filter = new CharacterFilter(true, 2);
assertEquals("aa bb", filter.filter("aaa bbb"));
}
}

View file

@ -2,12 +2,16 @@ package com.loki.lochat.core.filter;
import com.loki.lochat.core.filter.filters.SpamFilter;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class SpamFilterTest {
@ -15,11 +19,8 @@ class SpamFilterTest {
void filter_firstMessageAllowed() {
Player player = mock(Player.class);
when(player.hasPermission("lochat.bypass.spam")).thenReturn(false);
when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID());
YamlConfiguration config = new YamlConfiguration();
config.set("filters.spam.max-similar-messages", 3);
config.set("filters.spam.similarity-threshold", 80);
SpamFilter filter = new SpamFilter(config);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
SpamFilter filter = new SpamFilter(3, 80, "no spam");
FilterResult result = filter.filter(player, "hello");
assertTrue(result.allowed());
}
@ -28,12 +29,8 @@ class SpamFilterTest {
void filter_sameMessageRepeated() {
Player player = mock(Player.class);
when(player.hasPermission("lochat.bypass.spam")).thenReturn(false);
when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID());
YamlConfiguration config = new YamlConfiguration();
config.set("filters.spam.max-similar-messages", 2);
config.set("filters.spam.similarity-threshold", 80);
config.set("filters.spam.block-message", "no spam");
SpamFilter filter = new SpamFilter(config);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
SpamFilter filter = new SpamFilter(2, 80, "no spam");
filter.filter(player, "hello");
filter.filter(player, "hello");
FilterResult result = filter.filter(player, "hello");
@ -45,11 +42,8 @@ class SpamFilterTest {
void filter_differentMessagesAllowed() {
Player player = mock(Player.class);
when(player.hasPermission("lochat.bypass.spam")).thenReturn(false);
when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID());
YamlConfiguration config = new YamlConfiguration();
config.set("filters.spam.max-similar-messages", 2);
config.set("filters.spam.similarity-threshold", 80);
SpamFilter filter = new SpamFilter(config);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
SpamFilter filter = new SpamFilter(2, 80, "no spam");
filter.filter(player, "hello");
filter.filter(player, "world");
FilterResult result = filter.filter(player, "foo");
@ -60,9 +54,8 @@ class SpamFilterTest {
void filter_bypassPermission() {
Player player = mock(Player.class);
when(player.hasPermission("lochat.bypass.spam")).thenReturn(true);
when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID());
YamlConfiguration config = new YamlConfiguration();
SpamFilter filter = new SpamFilter(config);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
SpamFilter filter = new SpamFilter(3, 80, "no spam");
FilterResult result = filter.filter(player, "spam");
assertTrue(result.allowed());
}
@ -71,12 +64,8 @@ class SpamFilterTest {
void filter_historyLimit() {
Player player = mock(Player.class);
when(player.hasPermission("lochat.bypass.spam")).thenReturn(false);
when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID());
YamlConfiguration config = new YamlConfiguration();
config.set("filters.spam.max-similar-messages", 2);
config.set("filters.spam.similarity-threshold", 100);
config.set("filters.spam.block-message", "no spam");
SpamFilter filter = new SpamFilter(config);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
SpamFilter filter = new SpamFilter(2, 100, "no spam");
// Send 12 different messages to fill and rotate history
for (int i = 0; i < 12; i++) {
filter.filter(player, "msg" + i);
@ -92,12 +81,8 @@ class SpamFilterTest {
void filter_similarButNotExact() {
Player player = mock(Player.class);
when(player.hasPermission("lochat.bypass.spam")).thenReturn(false);
when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID());
YamlConfiguration config = new YamlConfiguration();
config.set("filters.spam.max-similar-messages", 2);
config.set("filters.spam.similarity-threshold", 90);
config.set("filters.spam.block-message", "no spam");
SpamFilter filter = new SpamFilter(config);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
SpamFilter filter = new SpamFilter(2, 90, "no spam");
filter.filter(player, "hello world");
filter.filter(player, "hello world");
FilterResult result = filter.filter(player, "hello world!");
@ -108,13 +93,10 @@ class SpamFilterTest {
@Test
void clearPlayer_removesData() {
Player player = mock(Player.class);
java.util.UUID uuid = java.util.UUID.randomUUID();
UUID uuid = UUID.randomUUID();
when(player.hasPermission("lochat.bypass.spam")).thenReturn(false);
when(player.getUniqueId()).thenReturn(uuid);
YamlConfiguration config = new YamlConfiguration();
config.set("filters.spam.max-similar-messages", 2);
config.set("filters.spam.similarity-threshold", 100);
SpamFilter filter = new SpamFilter(config);
SpamFilter filter = new SpamFilter(2, 100, "no spam");
filter.filter(player, "hello");
filter.clearPlayer(uuid);
filter.filter(player, "hello");