refactor: restructure services and filters architecture

- Remove deprecated service interfaces (CooldownService, IgnoreService, PMService, PlayerDataService, SpyService)
- Add new unified service interfaces (MessagingService, PlayerService, PunishmentService)
- Extract FilterResult as standalone class from AdvancedMessageFilter
- Implement individual filter classes (CharacterFilter, FloodFilter, SpamFilter, SwearFilter, UrlFilter)
- Add moderation commands (BanCommand, WarnCommand, SilentWarnCommand, UnbanCommand)
- Introduce data models for punishment tracking (BanRecord, WarnEntry)
- Refactor messaging service with new MessagingServiceImpl implementation
- Add mute service utilities (MuteDataStorage, MutePermissionChecker)
- Implement PunishmentServiceImpl with snapshot tracking
- Add Discord integration components (DiscordConfig, DiscordMessageService)
- Introduce command handlers for gradient module (ColorCommandHandler, PrefixCommandHandler)
- Add renderer components (EmojiComponent, GradientBuilder, UrlProcessor)
- Add utility classes (ColorConverter, MessageSender, PlaceholderProcessor)
- Update configuration files and plugin metadata
- Add documentation files (CODE_REVIEW_RU.md, REFACTORING_SERVICES.md)
- Consolidate service architecture for improved maintainability and separation of concerns
This commit is contained in:
loki5512344 2026-04-06 00:18:25 +02:00
parent 2d79d2a91d
commit bba480002f
181 changed files with 3455 additions and 2908 deletions

View file

@ -1,87 +1,36 @@
package com.loki.lochat.integrations;
import com.loki.lochat.util.FoliaUtil;
import org.bukkit.configuration.file.YamlConfiguration;
import com.loki.lochat.integrations.discord.DiscordConfig;
import com.loki.lochat.integrations.discord.DiscordMessageService;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.util.regex.Pattern;
/**
* Интеграция с Discord через вебхуки
* Интеграция с Discord - упрощенная версия
*/
public class DiscordIntegration {
private final JavaPlugin plugin;
private YamlConfiguration config;
private DiscordWebhook webhook;
private DiscordConfig config;
private DiscordMessageService messageService;
private boolean enabled = false;
// Паттерны для очистки сообщений
private static final Pattern MENTION_PATTERN = Pattern.compile("@(everyone|here)", Pattern.CASE_INSENSITIVE);
private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§[0-9a-fk-or]", Pattern.CASE_INSENSITIVE);
public DiscordIntegration(JavaPlugin plugin) {
this.plugin = plugin;
loadConfig();
initWebhook();
init();
}
/**
* Загрузить конфигурацию
*/
private void loadConfig() {
// Создаём папку config если её нет
File configFolder = new File(plugin.getDataFolder(), "config");
if (!configFolder.exists()) {
configFolder.mkdirs();
}
File configFile = new File(configFolder, "discord.yml");
if (!configFile.exists()) {
try {
// Копируем из resources/config/discord.yml
java.io.InputStream in = plugin.getResource("config/discord.yml");
if (in != null) {
java.nio.file.Files.copy(in, configFile.toPath());
in.close();
}
} catch (Exception e) {
plugin.getLogger().warning("Failed to copy config/discord.yml: " + e.getMessage());
}
}
config = YamlConfiguration.loadConfiguration(configFile);
enabled = config.getBoolean("enabled", false);
private void init() {
config = new DiscordConfig(plugin);
enabled = config.isEnabled();
if (!enabled) {
plugin.getLogger().info("Discord integration disabled in config");
return;
}
}
/**
* Инициализировать вебхук
*/
private void initWebhook() {
if (!enabled) return;
// Получаем конфигурацию внешнего вида для дефолтных значений
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
messageService = new DiscordMessageService(plugin, config);
String webhookUrl = config.getString("webhook.url", "");
String username = config.getString("webhook.username", "Minecraft Server");
String avatarUrl = config.getString("webhook.avatar_url", appearanceConfig.getDefaultAvatarUrl());
int timeout = config.getInt("performance.timeout", 10);
int retryAttempts = config.getInt("performance.retry_attempts", 3);
long retryDelay = config.getLong("performance.retry_delay", 1000);
// Логируем только факт наличия URL, но не его содержимое (security)
webhook = new DiscordWebhook(plugin, webhookUrl, username, avatarUrl, timeout, retryAttempts, retryDelay);
if (!webhook.isValid()) {
if (!messageService.isValid()) {
plugin.getLogger().severe("Invalid Discord webhook URL! Please check config/discord.yml");
enabled = false;
return;
@ -90,205 +39,47 @@ public class DiscordIntegration {
plugin.getLogger().info("Discord integration initialized successfully");
}
/**
* Отправить сообщение чата в Discord
*/
public void sendChatMessage(Player player, String message, boolean isGlobal) {
if (!enabled || !config.getBoolean("chat.enabled", true)) return;
if (!enabled || !config.isChatEnabled()) return;
// Проверяем фильтр глобального чата
if (config.getBoolean("chat.global_only", true) && !isGlobal) {
if (config.isGlobalOnly() && !isGlobal) {
return;
}
// Фильтрация сообщения
String cleanMessage = sanitizeMessage(message);
if (cleanMessage.length() < config.getInt("filter.min_message_length", 1)) {
return;
}
// Обрезаем если слишком длинное
int maxLength = config.getInt("filter.max_message_length", 2000);
if (cleanMessage.length() > maxLength) {
cleanMessage = cleanMessage.substring(0, maxLength - 3) + "...";
}
String playerName = player.getName();
String format = config.getString("chat.format", "**{player}**: {message}");
String formattedMessage = format
.replace("{player}", playerName)
.replace("{message}", cleanMessage);
// Отправляем асинхронно
if (config.getBoolean("performance.async", true)) {
FoliaUtil.runAsync(plugin, () -> sendChatMessageInternal(formattedMessage, playerName, isGlobal));
} else {
sendChatMessageInternal(formattedMessage, playerName, isGlobal);
}
messageService.sendChatMessage(player, message, isGlobal);
}
/**
* Внутренний метод отправки сообщения чата
*/
private void sendChatMessageInternal(String message, String playerName, boolean isGlobal) {
if (config.getBoolean("chat.use_embed", true)) {
// Получаем заголовки из конфигурации внешнего вида
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String title = isGlobal ?
appearanceConfig.getDiscordEventTitle("global_chat") :
appearanceConfig.getDiscordEventTitle("local_chat");
// Если заголовок не найден в новой конфигурации, используем старый способ
if (title.isEmpty()) {
title = isGlobal ? "🌍 Глобальный чат" : "📍 Локальный чат";
}
String color = config.getString("chat.embed_color", appearanceConfig.getDefaultEmbedColor());
String thumbnail = appearanceConfig.getPlayerAvatarUrl().replace("{player}", playerName);
webhook.sendEmbed(title, message, color, thumbnail);
} else {
webhook.sendMessage(message);
}
}
/**
* Отправить событие входа игрока
*/
public void sendPlayerJoin(Player player) {
if (!enabled || !config.getBoolean("events.enabled", true) ||
!config.getBoolean("events.join.enabled", true)) return;
// Получаем конфигурацию внешнего вида
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String playerName = player.getName();
String title = config.getString("events.join.title", appearanceConfig.getDiscordEventTitle("join"));
String description = config.getString("events.join.description", "**{player}** присоединился к серверу")
.replace("{player}", playerName);
String color = config.getString("events.join.color", "00FF00");
String thumbnail = config.getString("events.join.thumbnail", appearanceConfig.getPlayerAvatarUrl())
.replace("{player}", playerName);
if (config.getBoolean("performance.async", true)) {
FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(title, description, color, thumbnail));
} else {
webhook.sendEmbed(title, description, color, thumbnail);
}
if (!enabled || !config.isEventEnabled("join")) return;
messageService.sendPlayerJoin(player);
}
/**
* Отправить событие выхода игрока
*/
public void sendPlayerQuit(Player player) {
if (!enabled || !config.getBoolean("events.enabled", true) ||
!config.getBoolean("events.quit.enabled", true)) return;
// Получаем конфигурацию внешнего вида
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String playerName = player.getName();
String title = config.getString("events.quit.title", appearanceConfig.getDiscordEventTitle("quit"));
String description = config.getString("events.quit.description", "**{player}** покинул сервер")
.replace("{player}", playerName);
String color = config.getString("events.quit.color", "FF0000");
String thumbnail = config.getString("events.quit.thumbnail", appearanceConfig.getPlayerAvatarUrl())
.replace("{player}", playerName);
if (config.getBoolean("performance.async", true)) {
FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(title, description, color, thumbnail));
} else {
webhook.sendEmbed(title, description, color, thumbnail);
}
if (!enabled || !config.isEventEnabled("quit")) return;
messageService.sendPlayerQuit(player);
}
/**
* Отправить событие смерти игрока
*/
public void sendPlayerDeath(Player player, String deathMessage) {
if (!enabled || !config.getBoolean("events.enabled", true) ||
!config.getBoolean("events.death.enabled", true)) return;
// Получаем конфигурацию внешнего вида
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String playerName = player.getName();
String title = config.getString("events.death.title", appearanceConfig.getDiscordEventTitle("death"));
String description = config.getString("events.death.description", "**{player}** {death_message}")
.replace("{player}", playerName)
.replace("{death_message}", sanitizeMessage(deathMessage));
String color = config.getString("events.death.color", "800080");
String thumbnail = config.getString("events.death.thumbnail", appearanceConfig.getPlayerAvatarUrl())
.replace("{player}", playerName);
if (config.getBoolean("performance.async", true)) {
FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(title, description, color, thumbnail));
} else {
webhook.sendEmbed(title, description, color, thumbnail);
}
if (!enabled || !config.isEventEnabled("death")) return;
messageService.sendPlayerDeath(player, deathMessage);
}
/**
* Очистить сообщение от нежелательных элементов
*/
private String sanitizeMessage(String message) {
if (message == null) return "";
// Удаляем цветовые коды Minecraft
message = COLOR_CODE_PATTERN.matcher(message).replaceAll("");
// Заменяем опасные упоминания
if (config.getBoolean("filter.sanitize_mentions", true)) {
message = MENTION_PATTERN.matcher(message).replaceAll("@\u200B$1"); // Добавляем невидимый символ
}
// Экранируем Discord markdown
message = message.replace("*", "\\*")
.replace("_", "\\_")
.replace("`", "\\`")
.replace("~", "\\~");
return message.trim();
public void sendTestMessage(String message, String senderName) {
if (!enabled) return;
messageService.sendTestMessage(message, senderName);
}
/**
* Перезагрузить конфигурацию
*/
public void reload() {
loadConfig();
initWebhook();
init();
plugin.getLogger().info("Discord integration reloaded");
}
/**
* Отправить тестовое сообщение
*/
public void sendTestMessage(String message, String senderName) {
if (!enabled) return;
String title = "🧪 Тестовое сообщение";
String description = "**Отправитель:** " + senderName + "\n**Сообщение:** " + message;
String color = "FFD700"; // золотой цвет
String thumbnail = "https://mc-heads.net/avatar/" + senderName + "/64";
webhook.sendEmbed(title, description, color, thumbnail);
}
/**
* Остановить executor при выключении плагина
*/
public void shutdown() {
if (webhook != null) webhook.shutdown();
if (messageService != null) {
messageService.shutdown();
}
}
/**
* Проверить включена ли интеграция
*/
public boolean isEnabled() {
return enabled;
}

View file

@ -2,9 +2,7 @@ package com.loki.lochat.integrations;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChatService;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.api.service.PMService;
import com.loki.lochat.api.service.SpyService;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.gradient.GradientModule;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.Bukkit;
@ -19,17 +17,13 @@ import java.util.UUID;
public class PlaceholderAPIHook extends PlaceholderExpansion {
private final LoChat plugin;
private final IgnoreService ignoreService;
private final MessagingService messagingService;
private final ChatService chatService;
private final PMService pmService;
private final SpyService spyService;
public PlaceholderAPIHook(LoChat plugin) {
this.plugin = plugin;
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
this.messagingService = plugin.getServiceRegistry().get(MessagingService.class);
this.chatService = plugin.getServiceRegistry().get(ChatService.class);
this.pmService = plugin.getServiceRegistry().get(PMService.class);
this.spyService = plugin.getServiceRegistry().get(SpyService.class);
}
@Override
@ -60,15 +54,15 @@ public class PlaceholderAPIHook extends PlaceholderExpansion {
GradientModule gradient = plugin.getGradientModule();
return switch (params.toLowerCase()) {
case "ignored_count" -> String.valueOf(ignoreService.getIgnoredCount(uuid));
case "ignored_count" -> String.valueOf(messagingService.getIgnoredCount(uuid));
case "global_enabled" -> String.valueOf(!chatService.isGlobalChatDisabled(uuid));
case "last_pm" -> {
Optional<UUID> lastOpt = pmService.getLastConversation(uuid);
Optional<UUID> lastOpt = messagingService.getLastConversation(uuid);
if (lastOpt.isEmpty()) yield "";
var lastPlayer = Bukkit.getOfflinePlayer(lastOpt.get());
yield lastPlayer.getName() != null ? lastPlayer.getName() : "";
}
case "spy_enabled" -> String.valueOf(spyService.isSpying(uuid));
case "spy_enabled" -> String.valueOf(messagingService.isSpying(uuid));
// Gradient placeholders (совместимость с LoPreff)
// Используем TAB формат §x§R§R§G§G§B§B для совместимости с TAB плагином

View file

@ -0,0 +1,126 @@
package com.loki.lochat.integrations.discord;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
/**
* Конфигурация Discord интеграции
*/
public class DiscordConfig {
private final JavaPlugin plugin;
private YamlConfiguration config;
public DiscordConfig(JavaPlugin plugin) {
this.plugin = plugin;
load();
}
private void load() {
File configFolder = new File(plugin.getDataFolder(), "config");
if (!configFolder.exists()) {
configFolder.mkdirs();
}
File configFile = new File(configFolder, "discord.yml");
if (!configFile.exists()) {
try {
java.io.InputStream in = plugin.getResource("config/discord.yml");
if (in != null) {
java.nio.file.Files.copy(in, configFile.toPath());
in.close();
}
} catch (Exception e) {
plugin.getLogger().warning("Failed to copy config/discord.yml: " + e.getMessage());
}
}
config = YamlConfiguration.loadConfiguration(configFile);
}
public boolean isEnabled() {
return config.getBoolean("enabled", false);
}
public String getWebhookUrl() {
return config.getString("webhook.url", "");
}
public String getUsername() {
return config.getString("webhook.username", "Minecraft Server");
}
public String getAvatarUrl() {
return config.getString("webhook.avatar_url", "");
}
public int getTimeout() {
return config.getInt("performance.timeout", 10);
}
public int getRetryAttempts() {
return config.getInt("performance.retry_attempts", 3);
}
public long getRetryDelay() {
return config.getLong("performance.retry_delay", 1000);
}
public boolean isAsync() {
return config.getBoolean("performance.async", true);
}
public boolean isChatEnabled() {
return config.getBoolean("chat.enabled", true);
}
public boolean isGlobalOnly() {
return config.getBoolean("chat.global_only", true);
}
public boolean useEmbed() {
return config.getBoolean("chat.use_embed", true);
}
public String getChatFormat() {
return config.getString("chat.format", "**{player}**: {message}");
}
public String getEmbedColor() {
return config.getString("chat.embed_color", "5865F2");
}
public boolean isEventEnabled(String event) {
return config.getBoolean("events.enabled", true) &&
config.getBoolean("events." + event + ".enabled", true);
}
public String getEventTitle(String event) {
return config.getString("events." + event + ".title", "");
}
public String getEventDescription(String event) {
return config.getString("events." + event + ".description", "");
}
public String getEventColor(String event) {
return config.getString("events." + event + ".color", "5865F2");
}
public String getEventThumbnail(String event) {
return config.getString("events." + event + ".thumbnail", "");
}
public int getMinMessageLength() {
return config.getInt("filter.min_message_length", 1);
}
public int getMaxMessageLength() {
return config.getInt("filter.max_message_length", 2000);
}
public boolean sanitizeMentions() {
return config.getBoolean("filter.sanitize_mentions", true);
}
}

View file

@ -0,0 +1,166 @@
package com.loki.lochat.integrations.discord;
import com.loki.lochat.integrations.DiscordWebhook;
import com.loki.lochat.utils.FoliaUtil;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.regex.Pattern;
/**
* Сервис для отправки сообщений в Discord
*/
public class DiscordMessageService {
private static final Pattern MENTION_PATTERN = Pattern.compile("@(everyone|here)", Pattern.CASE_INSENSITIVE);
private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§[0-9a-fk-or]", Pattern.CASE_INSENSITIVE);
private final JavaPlugin plugin;
private final DiscordConfig config;
private final DiscordWebhook webhook;
public DiscordMessageService(JavaPlugin plugin, DiscordConfig config) {
this.plugin = plugin;
this.config = config;
this.webhook = new DiscordWebhook(
plugin,
config.getWebhookUrl(),
config.getUsername(),
getAvatarUrl(),
config.getTimeout(),
config.getRetryAttempts(),
config.getRetryDelay()
);
}
private String getAvatarUrl() {
String url = config.getAvatarUrl();
if (url.isEmpty()) {
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
return loChat.getConfigManager().getAppearanceConfig().getDefaultAvatarUrl();
}
return url;
}
public void sendChatMessage(Player player, String message, boolean isGlobal) {
String cleanMessage = sanitizeMessage(message);
if (cleanMessage.length() < config.getMinMessageLength()) {
return;
}
if (cleanMessage.length() > config.getMaxMessageLength()) {
cleanMessage = cleanMessage.substring(0, config.getMaxMessageLength() - 3) + "...";
}
String formatted = config.getChatFormat()
.replace("{player}", player.getName())
.replace("{message}", cleanMessage);
if (config.isAsync()) {
FoliaUtil.runAsync(plugin, () -> sendFormatted(formatted, player, isGlobal));
} else {
sendFormatted(formatted, player, isGlobal);
}
}
private void sendFormatted(String message, Player player, boolean isGlobal) {
if (config.useEmbed()) {
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String title = isGlobal ?
appearanceConfig.getDiscordEventTitle("global_chat") :
appearanceConfig.getDiscordEventTitle("local_chat");
if (title.isEmpty()) {
title = isGlobal ? "🌍 Глобальный чат" : "📍 Локальный чат";
}
String color = config.getEmbedColor();
String thumbnail = appearanceConfig.getPlayerAvatarUrl().replace("{player}", player.getName());
webhook.sendEmbed(title, message, color, thumbnail);
} else {
webhook.sendMessage(message);
}
}
public void sendPlayerJoin(Player player) {
sendEvent(player, "join", null);
}
public void sendPlayerQuit(Player player) {
sendEvent(player, "quit", null);
}
public void sendPlayerDeath(Player player, String deathMessage) {
sendEvent(player, "death", deathMessage);
}
private void sendEvent(Player player, String event, String extraMessage) {
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String title = config.getEventTitle(event);
if (title.isEmpty()) {
title = appearanceConfig.getDiscordEventTitle(event);
}
String description = config.getEventDescription(event)
.replace("{player}", player.getName());
if (extraMessage != null) {
description = description.replace("{death_message}", sanitizeMessage(extraMessage));
}
String color = config.getEventColor(event);
String thumbnail = config.getEventThumbnail(event);
if (thumbnail.isEmpty()) {
thumbnail = appearanceConfig.getPlayerAvatarUrl();
}
final String finalThumbnail = thumbnail.replace("{player}", player.getName());
final String finalTitle = title;
final String finalDescription = description;
final String finalColor = color;
if (config.isAsync()) {
FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(finalTitle, finalDescription, finalColor, finalThumbnail));
} else {
webhook.sendEmbed(title, description, color, finalThumbnail);
}
}
public void sendTestMessage(String message, String senderName) {
String title = "🧪 Тестовое сообщение";
String description = "**Отправитель:** " + senderName + "\n**Сообщение:** " + message;
String color = "FFD700";
String thumbnail = "https://mc-heads.net/avatar/" + senderName + "/64";
webhook.sendEmbed(title, description, color, thumbnail);
}
private String sanitizeMessage(String message) {
if (message == null) return "";
message = COLOR_CODE_PATTERN.matcher(message).replaceAll("");
if (config.sanitizeMentions()) {
message = MENTION_PATTERN.matcher(message).replaceAll("@\u200B$1");
}
message = message.replace("*", "\\*")
.replace("_", "\\_")
.replace("`", "\\`")
.replace("~", "\\~");
return message.trim();
}
public boolean isValid() {
return webhook.isValid();
}
public void shutdown() {
webhook.shutdown();
}
}