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

356
CODE_REVIEW_RU.md Normal file
View file

@ -0,0 +1,356 @@
# 🔍 Code Review: Проблемы архитектуры (SOLID, DRY, KISS)
## 🔴 Критические проблемы
### 1. DRY: Дублирование кода в конфигах
**Файлы:** `AppearanceConfig.java`, `MessagesConfig.java`, `FiltersConfig.java`
**Проблема:**
- Каждый конфиг содержит 30-50 геттеров с одинаковой структурой
- ~300 строк повторяющегося кода
- При изменении логики нужно править в 3 местах
**Решение:**
```java
// Создать базовый класс с generic методами
public abstract class BaseConfig {
protected String getString(String path, String def) { ... }
protected int getInt(String path, int def) { ... }
protected boolean getBoolean(String path, boolean def) { ... }
}
// Использовать в конфигах
public class MessagesConfig extends BaseConfig {
public String getNoPermission() {
return getString("general.no_permission", "&#CF6679Нет прав!");
}
}
```
---
### 2. SRP: ConfigManager делает слишком много
**Файл:** `ConfigManager.java`
**Проблема:**
- Управляет 5 конфигами
- Содержит логику обновления версий
- Имеет 30+ геттеров для настроек
- Нарушает Single Responsibility Principle
**Решение:**
```java
// Разделить на отдельные классы
class ConfigManager {
private final AppearanceConfig appearance;
private final MessagesConfig messages;
// Только управление конфигами
}
class ConfigUpdater {
// Логика обновления версий
void updateConfig(FileConfiguration config) { ... }
}
```
---
### 3. KISS: ColorConverter неэффективен
**Файл:** `ColorConverter.java`
**Проблема:**
```java
private static String convertAmpersandColors(String message) {
return message
.replaceAll("&0", "<black>")
.replaceAll("&1", "<dark_blue>")
// ... 16 вызовов replaceAll
}
```
- Каждый `replaceAll` компилирует regex
- O(n * 16) сложность
- Дублирование для `§` и `&`
**Решение:**
```java
private static final Map<String, String> COLOR_MAP = Map.ofEntries(
Map.entry("&0", "<black>"),
Map.entry("§0", "<black>"),
Map.entry("&1", "<dark_blue>"),
Map.entry("§1", "<dark_blue>")
// ...
);
private static String convertColors(String message) {
for (Map.Entry<String, String> entry : COLOR_MAP.entrySet()) {
message = message.replace(entry.getKey(), entry.getValue());
}
return message;
}
```
---
### 4. DRY: Дублирование проверок permissions
**Файл:** `AdvancedMessageFilter.java`
**Проблема:**
```java
if (!player.hasPermission("lochat.bypass.swear") && !player.hasPermission("lochat.bypass.filter"))
if (!player.hasPermission("lochat.bypass.flood") && !player.hasPermission("lochat.bypass.filter"))
if (!player.hasPermission("lochat.bypass.spam") && !player.hasPermission("lochat.bypass.filter"))
```
**Решение:**
```java
private boolean canBypassFilter(Player player, String filterType) {
return player.hasPermission("lochat.bypass." + filterType)
|| player.hasPermission("lochat.bypass.filter");
}
// Использование
if (!canBypassFilter(player, "swear")) {
FilterResult swear = swearFilter.filter(player, message);
// ...
}
```
---
### 5. OCP: EnhancedChatRenderer не расширяем
**Файл:** `EnhancedChatRenderer.java`
**Проблема:**
```java
switch (placeholder) {
case "emoji" -> result = result.append(emoji);
case "prefix" -> result = result.append(prefix);
// Добавление нового плейсхолдера требует изменения кода
}
```
**Решение:**
```java
// Strategy pattern для плейсхолдеров
interface PlaceholderResolver {
Component resolve(Player player, boolean isGlobal);
}
Map<String, PlaceholderResolver> resolvers = Map.of(
"emoji", (p, g) -> buildEmojiComponent(g),
"prefix", (p, g) -> buildPrefix(g),
"player", (p, g) -> buildPlayerComponent(p)
);
// Использование
Component resolved = resolvers.getOrDefault(placeholder,
(p, g) -> Component.text("{" + placeholder + "}"))
.resolve(player, isGlobal);
```
---
### 6. ISP: MuteServiceImpl не реализован полностью
**Файл:** `MuteServiceImpl.java`
**Проблема:**
```java
@Override
public void save() {
// TODO: Implement save to file
}
private void load() {
// TODO: Implement load from file
}
```
**Решение:**
- Либо реализовать методы
- Либо разделить интерфейс на `MuteService` и `PersistentMuteService`
---
### 7. Отсутствует зависимость VoiceChat
**Файл:** `MuteServiceImpl.java`
**Проблема:**
```java
import de.maxhenkel.voicechat.api.BukkitVoicechatService; // ❌ Не найден
```
**Решение:**
```gradle
// build.gradle.kts
dependencies {
compileOnly("de.maxhenkel.voicechat:voicechat-api:2.5.0")
}
```
Или сделать опциональной зависимостью:
```java
private void initVoiceChat() {
try {
Class.forName("de.maxhenkel.voicechat.api.BukkitVoicechatService");
// Инициализация
} catch (ClassNotFoundException e) {
plugin.getLogger().info("VoiceChat not found, voice mute disabled");
}
}
```
---
### 8. SRP: ChatEventListener слишком сложный
**Файл:** `ChatEventListener.java`
**Проблема:**
- Метод `onChat` содержит 80+ строк
- Обрабатывает: режим чата, фильтрацию, радиус, Discord, статистику
- Нарушает Single Responsibility
**Решение:**
```java
class ChatEventListener {
private final ChatModeHandler modeHandler;
private final MessageFilterHandler filterHandler;
private final LocalChatHandler localHandler;
private final DiscordHandler discordHandler;
private final StatsHandler statsHandler;
@EventHandler
public void onChat(AsyncChatEvent event) {
Player sender = event.getPlayer();
String message = extractMessage(event);
// Делегируем обработку
ChatMode mode = modeHandler.determineMode(message);
if (!filterHandler.filter(sender, message)) {
event.setCancelled(true);
return;
}
if (mode == ChatMode.LOCAL) {
localHandler.handleLocal(event, sender);
}
discordHandler.sendToDiscord(sender, message, mode);
statsHandler.recordMessage(sender, mode);
}
}
```
---
### 9. DRY: Дублирование конвертации цветов
**Файл:** `ColorConverter.java`
**Проблема:**
```java
private static String convertSectionColors(String message) { ... }
private static String convertAmpersandColors(String message) { ... }
```
Идентичная логика, только разные символы.
**Решение:**
```java
private static String convertColors(String message, char symbol) {
return message
.replaceAll(symbol + "0", "<black>")
.replaceAll(symbol + "1", "<dark_blue>")
// ...
}
public static String convertLegacyFormats(String message) {
message = convertColors(message, '§');
message = convertColors(message, '&');
return message;
}
```
---
### 10. DIP: Прямая зависимость от LoChat
**Файлы:** `EnhancedChatRenderer.java`, `ChatEventListener.java`, и др.
**Проблема:**
```java
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
```
Нарушает Dependency Inversion Principle.
**Решение:**
```java
// Создать интерфейс
interface ChatPlugin {
ConfigManager getConfigManager();
GradientModule getGradientModule();
DiscordIntegration getDiscordIntegration();
}
// LoChat реализует интерфейс
class LoChat extends JavaPlugin implements ChatPlugin { ... }
// Использование
class EnhancedChatRenderer {
private final ChatPlugin plugin;
public EnhancedChatRenderer(ChatPlugin plugin, boolean isGlobal) {
this.plugin = plugin;
// Нет каста
}
}
```
---
## 📊 Статистика проблем
| Принцип | Нарушений | Критичность |
|---------|-----------|-------------|
| DRY | 4 | 🔴 Высокая |
| SRP | 3 | 🔴 Высокая |
| KISS | 2 | 🟡 Средняя |
| OCP | 1 | 🟡 Средняя |
| DIP | 1 | 🟡 Средняя |
| ISP | 1 | 🟢 Низкая |
---
## ✅ Что сделано хорошо
1. **ServiceRegistry** - правильная реализация DI
2. **Разделение на модули** - gradient, integrations, core
3. **Использование интерфейсов** - api.service.*
4. **PluginInitializer/PluginShutdown** - разделение логики инициализации
5. **BaseConfig** - базовый класс для конфигов (уже есть!)
---
## 🎯 Приоритеты исправления
### Высокий приоритет
1. Исправить отсутствующую зависимость VoiceChat
2. Упростить ColorConverter (KISS)
3. Вынести проверки permissions в отдельный метод (DRY)
### Средний приоритет
4. Разделить ChatEventListener на handler'ы (SRP)
5. Убрать касты к LoChat через интерфейс (DIP)
6. Сделать EnhancedChatRenderer расширяемым (OCP)
### Низкий приоритет
7. Реализовать save/load в MuteServiceImpl (ISP)
8. Упростить ConfigManager (SRP)
9. Объединить методы конвертации цветов (DRY)
---
## 📝 Итого
Проект имеет хорошую базовую архитектуру (ServiceRegistry, модули, интерфейсы), но есть проблемы с:
- **Дублированием кода** (DRY) - особенно в конфигах и фильтрах
- **Слишком большими классами** (SRP) - ConfigManager, ChatEventListener
- **Неэффективными алгоритмами** (KISS) - ColorConverter
Рекомендую начать с исправления критических проблем (VoiceChat, ColorConverter, permissions), затем постепенно рефакторить остальное.

View file

@ -1,90 +0,0 @@
# План миграции команд на новую архитектуру
> **Статус (актуально для репозитория):** централизованная регистрация в `CommandManager` и раскладка по пакетам **уже внедрены**. Ниже — что сделано и что остаётся опциональной доработкой.
**Что осталось по плану:** миграция команд **завершена**. Дальше — по желанию: перевести оставшиеся `CommandExecutor` на `PlayerCommand`/`AdminCommand`, добавить тесты/ручной регресс, короткую доку «как добавить команду».
## Что уже сделано
### Архитектура
- `commands/base/BaseCommand`, `PlayerCommand`, `AdminCommand`
- `CommandManager` — единая точка регистрации (`reg` / `regTab` / `registerBaseCommand`)
### Пакеты (фактическая структура)
| Пакет | Назначение |
|--------|------------|
| `commands/chat/` | глобальный / локальный чат (`g`, `l`) |
| `commands/messaging/` | ЛС, игнор (`msg`, `reply`, `ignore`, `unignore`, `ignorelist`) |
| `commands/moderation/` | муты, варны, баны (`lmute`, `lunmute`, `lmutelist`, `lmutehistory`, `lmuteblame`, `warn`, `silentwarn`, `lban`, `lunban`) |
| `commands/nick/` | ник, инфо (`nick`, `playerinfo`) |
| `commands/admin/` | админ-утилиты (`announce`, `chatspy`, `clearchat`, `clearchatconfig`, `lochat`, `lochatreload`, `discordadmin`); в репозитории также есть `HubCommand.java` (проверьте `plugin.yml`, если нужна команда хаба) |
| `commands/rp/` | RP (`me`, `try`, `do`, `roll`) |
| Корень `commands/` | `CustomCommandsCommand`, `CustomCommand` |
Папки `social/` в проекте нет — социальные команды лежат в `messaging/` и `nick/`.
### Регистрация в `CommandManager` (выполнено)
- [x] Этап 1 — чат: `GlobalChatCommand`, `LocalChatCommand`
- [x] Этап 2 — ЛС: `MsgCommand`, `ReplyCommand`
- [x] Этап 3 — социальное: `IgnoreCommand`, `UnignoreCommand`, `IgnoreListCommand`, `NickCommand` (+ `PlayerInfoCommand`)
- [x] Этап 4 — модерация: все перечисленные в таблице пакета `moderation/`
- [x] Этап 5 — админ: `AnnounceCommand`, `ClearChatCommand`, `ClearChatConfigCommand`, `ChatSpyCommand`, `LoChatCommand`, `ReloadConfigCommand`, `DiscordCommand`
- [x] Этап 6 — `CustomCommandsCommand` и движок кастомных команд
## Что ещё можно сделать (не блокирует работу)
### Унификация базового класса
Многие команды по-прежнему реализуют `CommandExecutor` напрямую, а не `PlayerCommand` / `AdminCommand`. Имеет смысл постепенно переводить на базовые классы там, где это убирает дублирование проверок и сообщений.
### Тесты и ручная проверка
- [ ] Стабильные сценарии для приоритетных команд (чат, ЛС, муты) — ручные или автотесты
- [ ] Проверка TabCompleter на зарегистрированных командах с `regTab`
### Документация
- [ ] Короткий `CONTRIBUTING` или раздел в README: как добавить команду через `CommandManager` и `plugin.yml`
## Процесс миграции одной команды (если рефакторите дальше)
### 1. Новая команда на базовом классе
```java
public class NewCommand extends PlayerCommand {
public NewCommand(LoChat plugin) {
super(plugin);
}
@Override
protected boolean executePlayerCommand(Player player, Command command,
String label, String[] args) {
return true;
}
}
```
### 2. Проверки
- Поведение и права
- Сообщения из `MessagesConfig` / конфигов
- TabCompleter при необходимости
### 3. Регистрация
В `CommandManager`: `reg("name", new NewCommand(plugin))` или `regTab`, плюс запись в `plugin.yml`.
### 4. Удаление старого кода
После замены — удалить старый класс и неиспользуемые импорты.
## Чек-лист для команды
- [ ] Корректный базовый класс или явный `CommandExecutor` с единым стилем обработки ошибок
- [ ] Права из `plugin.yml`
- [ ] Сообщения из конфигурации
- [ ] Валидация аргументов
- [ ] TabCompleter при необходимости
## Ожидаемые эффекты от доведения до единого стиля
- Меньше дублирования проверок отправителя и прав
- Проще добавлять новые команды
- Проще сопровождать код при едином паттерне
---
**Следующие шаги (по желанию):** выбрать 2–3 самых «шумных» по коду команды → перевести на `PlayerCommand`/`AdminCommand` → добавить минимальные тесты или чек-лист ручной регрессии.

287
REFACTORING_SERVICES.md Normal file
View file

@ -0,0 +1,287 @@
# 🔧 Рефакторинг сервисов - уменьшение количества файлов
## Текущая ситуация
- **11 интерфейсов** в `api/service/`
- **12 реализаций** в `core/service/`
- **23 файла** только для сервисов
- Некоторые сервисы по 30 строк кода
## Проблема
Слишком много мелких файлов, которые можно объединить по функциональности.
---
## 🎯 План рефакторинга
### 1. Объединить простые сервисы в один
**Было:**
```
PMService.java (28 строк)
SpyService.java (69 строк)
IgnoreService.java (97 строк)
```
**Станет:**
```java
// MessagingService.java - всё что связано с общением
public interface MessagingService {
// PM
void setLastConversation(UUID sender, UUID receiver);
UUID getLastConversation(UUID player);
// Spy
boolean toggleSpy(UUID player);
boolean isSpying(UUID player);
void broadcastPM(Player sender, Player receiver, String message);
// Ignore
void addIgnore(UUID player, UUID target);
void removeIgnore(UUID player, UUID target);
boolean isIgnoring(UUID player, UUID target);
Set<UUID> getIgnoreList(UUID player);
}
```
**Экономия:** 3 интерфейса + 3 реализации = **6 файлов → 2 файла**
---
### 2. Объединить Mute + Punishment
**Было:**
```
MuteService.java + MuteServiceImpl.java (212 строк)
PunishmentService.java + PunishmentServiceImpl.java (173 строки)
MuteHistoryManager.java (77 строк)
```
**Станет:**
```java
// ModerationService.java - всё что связано с модерацией
public interface ModerationService {
// Mutes
void mute(UUID uuid, String name, long duration, String reason, String by);
boolean unmute(UUID uuid, String by);
boolean isMuted(UUID uuid);
MuteData getMuteData(UUID uuid);
// Bans
void ban(UUID uuid, String name, long duration, String reason, String by);
boolean unban(UUID uuid, String by);
boolean isBanned(UUID uuid);
BanData getBanData(UUID uuid);
// Warns
void warn(UUID uuid, String name, String reason, String by);
List<WarnData> getWarns(UUID uuid);
// History
List<ModerationEntry> getHistory(UUID uuid);
List<ModerationEntry> getHistoryByModerator(String name);
}
```
**Экономия:** 3 интерфейса + 3 реализации = **6 файлов → 2 файла**
---
### 3. Объединить Cooldown + PlayerData
**Было:**
```
CooldownService.java + CooldownServiceImpl.java (40 строк)
PlayerDataService.java + PlayerDataServiceImpl.java (102 строки)
```
**Станет:**
```java
// PlayerService.java - всё что связано с данными игрока
public interface PlayerService {
// Cooldowns
boolean hasCooldown(UUID player, String type);
void setCooldown(UUID player, String type, long duration);
long getRemainingCooldown(UUID player, String type);
void clearCooldowns(UUID player);
// Statistics
void recordMessage(UUID player, String chatType);
PlayerStats getStats(UUID player);
void saveAll();
}
```
**Экономия:** 2 интерфейса + 2 реализации = **4 файла → 2 файла**
---
### 4. Оставить как есть (нормальный размер)
```
ChatService.java + ChatServiceImpl.java (114 строк) ✅
MessageService.java + MessageServiceImpl.java (36 строк) ✅
MentionService.java + MentionServiceImpl.java (104 строки) ✅
NickService.java + NickServiceImpl.java (155 строк) ✅
```
Эти сервисы имеют чёткую ответственность и нормальный размер.
---
## 📊 Результат рефакторинга
| До | После | Экономия |
|----|-------|----------|
| 11 интерфейсов | 7 интерфейсов | -4 |
| 12 реализаций | 8 реализаций | -4 |
| **23 файла** | **15 файлов** | **-8 файлов** |
---
## 🔨 Пошаговая реализация
### Шаг 1: Создать MessagingService
```java
// api/service/MessagingService.java
public interface MessagingService {
// PM
void setLastConversation(UUID sender, UUID receiver);
UUID getLastConversation(UUID player);
// Spy
boolean toggleSpy(UUID player);
boolean isSpying(UUID player);
void broadcastPM(Player sender, Player receiver, String message);
void sendToSpies(Player sender, Component message, boolean isGlobal);
void removeSpy(UUID player);
// Ignore
void addIgnore(UUID player, UUID target);
void removeIgnore(UUID player, UUID target);
boolean isIgnoring(UUID player, UUID target);
Set<UUID> getIgnoreList(UUID player);
void save();
}
// core/service/MessagingServiceImpl.java
public class MessagingServiceImpl implements MessagingService {
private final JavaPlugin plugin;
private final MessageConfig messageConfig;
// PM state
private final Map<UUID, UUID> lastConversation = new ConcurrentHashMap<>();
// Spy state
private final Set<UUID> spyEnabled = ConcurrentHashMap.newKeySet();
// Ignore state
private final Map<UUID, Set<UUID>> ignoreMap = new ConcurrentHashMap<>();
public MessagingServiceImpl(JavaPlugin plugin, MessageConfig messageConfig) {
this.plugin = plugin;
this.messageConfig = messageConfig;
loadIgnoreData();
}
// Реализация всех методов...
}
```
### Шаг 2: Обновить ServiceRegistry
```java
// Было
register(PMService.class, new PMServiceImpl());
register(SpyService.class, new SpyServiceImpl(messageConfig));
register(IgnoreService.class, new IgnoreServiceImpl(plugin));
// Стало
MessagingService messaging = new MessagingServiceImpl(plugin, messageConfig);
register(MessagingService.class, messaging);
// Для обратной совместимости (опционально)
register(PMService.class, messaging);
register(SpyService.class, messaging);
register(IgnoreService.class, messaging);
```
### Шаг 3: Удалить старые файлы
```bash
# Удалить интерфейсы
rm src/main/java/com/loki/lochat/api/service/PMService.java
rm src/main/java/com/loki/lochat/api/service/SpyService.java
rm src/main/java/com/loki/lochat/api/service/IgnoreService.java
# Удалить реализации
rm src/main/java/com/loki/lochat/core/service/PMServiceImpl.java
rm src/main/java/com/loki/lochat/core/service/SpyServiceImpl.java
rm src/main/java/com/loki/lochat/core/service/IgnoreServiceImpl.java
```
---
## ⚠️ Альтернативный подход (если не хочешь ломать API)
Можно оставить старые интерфейсы, но сделать их делегатами:
```java
// Старый интерфейс остаётся
public interface PMService {
void setLastConversation(UUID sender, UUID receiver);
UUID getLastConversation(UUID player);
}
// Но реализация делегирует в MessagingService
public class PMServiceAdapter implements PMService {
private final MessagingService messaging;
public PMServiceAdapter(MessagingService messaging) {
this.messaging = messaging;
}
@Override
public void setLastConversation(UUID sender, UUID receiver) {
messaging.setLastConversation(sender, receiver);
}
@Override
public UUID getLastConversation(UUID player) {
return messaging.getLastConversation(player);
}
}
```
Это позволит:
- Сохранить обратную совместимость
- Уменьшить количество реализаций (3 → 1)
- Постепенно мигрировать код на новый API
---
## 🎯 Рекомендация
**Начни с MessagingService** - это самое простое объединение:
1. PM, Spy, Ignore логически связаны (общение между игроками)
2. Все три сервиса маленькие (28-97 строк)
3. Объединение даст ~200 строк - нормальный размер
**Потом ModerationService** - это сложнее:
1. Mute + Punishment + History = ~460 строк
2. Нужно будет рефакторить внутреннюю структуру
3. Но логически это одна область (модерация)
**PlayerService оставь на потом** - там всё норм.
---
## 📝 Итого
Объединение сервисов:
- ✅ Уменьшит количество файлов с 23 до 15
- ✅ Упростит навигацию по коду
- ✅ Сгруппирует связанную функциональность
- ⚠️ Потребует обновления кода, который использует старые сервисы
Начни с MessagingService - это быстро и безопасно.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more