fix: critical fixes from code review — async translation, atomic persistence, unified chat pipeline, config key alignment
This commit is contained in:
parent
103c982f87
commit
b1b0c9082b
31 changed files with 530 additions and 384 deletions
86
TODO.md
86
TODO.md
|
|
@ -1,6 +1,38 @@
|
|||
# LoChat Development TODO
|
||||
|
||||
> Last updated: 2026-07-05
|
||||
> Last updated: 2026-08-04
|
||||
|
||||
---
|
||||
|
||||
## ✅ CRITICAL FIXES (blocking release — from code review 2026-08-04, all DONE)
|
||||
|
||||
### 1. Translation: async + bounded cache, no String.intern(), out of render() ✅
|
||||
- `renderer/EnhancedChatRenderer.java` — removed blocking `applyTranslation()` from `render()` (zero network I/O in render path). Translation is now delivered via async action bar (`sendTranslationAsync` → `translateAsync().thenAccept`).
|
||||
- `translate/TranslationService.java` — no more `String.intern()`; bounded LRU cache (2000 entries, synchronized LinkedHashMap); `translateAsync` runs on a dedicated 2-thread daemon executor; failures return original text, never throw.
|
||||
|
||||
### 2. Atomic file writes (temp + rename, per-file lock) for all persistence ✅
|
||||
- `utils/persistence/FilePersistence.java` — `saveYaml`/`saveJson` now write `<name>.tmp` + `Files.move(ATOMIC_MOVE, REPLACE_EXISTING)` under a per-file `ReentrantLock`.
|
||||
- `core/service/PlayerServiceImpl.java` — `statsLock` serializes async `persistPlayerMessages` and main-thread `saveAll()`.
|
||||
- `core/service/PunishmentServiceImpl.java` — removed `saveAsync()` from `isBanned()` read path.
|
||||
- `core/service/mute/MuteDataStorage.java` — uses `FilePersistence.saveJson` (atomic), dropped raw FileWriter.
|
||||
- `core/service/mute/MuteHistoryManager.java` — atomic temp+rename write under per-file lock, no `e.printStackTrace()`.
|
||||
|
||||
### 3. Route /g and /l through the same filter pipeline + escape MiniMessage ✅
|
||||
- `core/service/ChatServiceImpl.java` — `String` messages now run length check → `MessageService.processMessage` (mute/cooldown) → `AdvancedMessageFilter` → `MiniMessage.escapeTags()` → component (colors only with `lochat.chat.colors`). `Component` messages bypass (trusted internal). Local radius from `AppearanceConfig`.
|
||||
- `commands/chat/GlobalChatCommand.java`, `LocalChatCommand.java` — pass plain `String`, no more `MM.deserialize(raw input)`.
|
||||
- `LoChat.java`/`PluginInitializer.java` — filter instance stored on plugin; `ServiceRegistry` builds `MessageService` before `ChatService`.
|
||||
|
||||
### 4. Remove duplicate Discord send; single global/local detection ✅
|
||||
- `listener/DiscordEventListener.java` — deleted the `onChat(AsyncChatEvent)` handler; `ChatEventListener` is now the single send path with correct `isGlobal`.
|
||||
|
||||
### 5. Fix config key mismatches (underscore vs dash / nesting) ✅
|
||||
- `config/AppearanceConfig.java` — `chat.local-radius`, `chat.clear-lines`, `chat.min-message-length`, `chat.max-message-length`, `chat.colors.*` (dash), `prefixes.*.separator` (string), `discord.avatar-urls.*`, `discord.event-titles.*` (+ `_`→`-` normalization).
|
||||
- `integrations/discord/DiscordConfig.java` — `webhook.avatar-url`, `chat.global-only`, `chat.use-embed`, `chat.embed-color`, `filter.*` (dash), `performance.retry-*`.
|
||||
- `config/MuteConfig.java` — `settings.*`, `notifications.*`.
|
||||
- `config/SoundsConfig.java` — `messages.mention.*`, `messages.private-message.*`, `commands.success.*`.
|
||||
- `config/FiltersConfig.java` + filters — `config/filters.yml` is now the single source of truth (`caps.max-caps-percent`, `swear.replace`, `hidden-urls.*`, `spam.max-repeating-chars`, `flood.max-same-messages`).
|
||||
- `gradient/config/GradientConfig.java` — nested keys `gradient.limits.*`, `gradient.pricing.*`, `gradient.cooldowns.*`, `gradient.formatting.*`, `gradient.storage.type` → **SQLite now detected**.
|
||||
- `managers/AutoMessageManager.java` + `config.yml` — reads `automessages.messages` (new `automessages:` section added to config.yml), honors `automessages.random`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -32,7 +64,34 @@
|
|||
|
||||
---
|
||||
|
||||
## 🟡 REMAINING (COULD DO)
|
||||
## 🟡 NEXT: NEW FEATURES
|
||||
|
||||
### 1. Channels (like VentureChat / Carbon)
|
||||
- `/join`, `/leave`, `/channel` — full channel system: global, local, staff, trade, party.
|
||||
- Per-channel colors, aliases, permissions, cooldowns, spy mode.
|
||||
- Party chat: private groups `/party`.
|
||||
- Network channels (Bungee/Velocity) sync.
|
||||
|
||||
### 2. Anti-swear hardening + filter statistics
|
||||
- Bypass obfuscation: `q→р` leet/Unicode substitution, repeated chars between letters, spaces/dots inside words.
|
||||
- Track filter stats (how many messages blocked per filter) → expose via command/placeholder.
|
||||
|
||||
### 3. Stable plugin API + API events
|
||||
- Publish `api/` modules to a repo so third-party plugins can depend on it.
|
||||
- Add `api-version` hint / `provides` in `plugin.yml` for dependency resolution.
|
||||
- Dedicated API events: `LoChatMessageEvent`, `LoChatFilterEvent`, `LoChatPunishEvent` etc.
|
||||
|
||||
### 4. DiscordSRV-style 2-way bridge (Discord → chat)
|
||||
- Read messages from a Discord channel (bot or webhook listener) and relay into Minecraft chat.
|
||||
- Channel ↔ Discord channel mapping.
|
||||
|
||||
### 5. Full spy + auto-mute for spam
|
||||
- PM spy + command spy with custom formats (like VentureChat).
|
||||
- Auto-mute repeat offenders (spam/flood) with configurable threshold and duration.
|
||||
|
||||
---
|
||||
|
||||
## 🔵 LOWER PRIORITY (carry-over)
|
||||
|
||||
### 1. Shared Persistence Utility
|
||||
5 services repeat load/save/ensureDir pattern:
|
||||
|
|
@ -40,8 +99,8 @@
|
|||
|
||||
**Fix:** Extract `FilePersistenceUtil` or base class
|
||||
|
||||
### 2. Mute Duration Permissions Not Declared in plugin.yml
|
||||
`lochat.mute.dur.5m`, `lochat.mute.dur.3h`, `lochat.mute.reason.*` — used in code/configs but missing from plugin.yml declarations.
|
||||
### 2. Persist nicknames on change
|
||||
`NickServiceImpl.java:77` mutates only in-memory map; `save()` only on shutdown → nicknames lost on crash (async save after mutation).
|
||||
|
||||
### 3. DIP: Service Locator Anti-Pattern
|
||||
`ServiceRegistry.get(Xxx.class)` used everywhere instead of constructor DI. Big refactor.
|
||||
|
|
@ -49,20 +108,17 @@
|
|||
### 4. DRY: PM Send Logic Duplicated
|
||||
`MsgCommand` + `ReplyCommand` still have near-identical send logic.
|
||||
|
||||
### 5. PM Send Logic Duplicated Between MsgCommand and ReplyCommand
|
||||
### 5. Fix cooldown map collision
|
||||
`PlayerServiceImpl.java:80-82` — all non-global types (local chat + RP `/me /do /try /roll`) share one map → cross-blocking cooldowns. Split per-type maps.
|
||||
|
||||
---
|
||||
### 6. Thread-safe flood/spam deques
|
||||
`FloodFilter.java:32`, `SpamFilter.java:30` use plain `ArrayDeque` written from async threads, read from main → use synchronized/thread-safe structures.
|
||||
|
||||
## 🔵 LOW PRIORITY
|
||||
### 7. Tests
|
||||
Only 11 test files for 155 main files. Very low coverage (JaCoCo wired, CI uploads report).
|
||||
|
||||
### 6. Tests
|
||||
Only 4 test files for 150+ main files. Very low coverage.
|
||||
|
||||
### 7. Aho-Corasick for Swear Filter
|
||||
Performance optimization for word filtering.
|
||||
|
||||
### 8. Discord Rate Limiter
|
||||
Prevent rate limiting when sending many webhook messages.
|
||||
### 8. No PM cooldown
|
||||
`chat.pm.cooldown: 2` in config.yml is never read → `/msg` spam unthrottled.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ public class PunishmentServiceImpl implements PunishmentService {
|
|||
}
|
||||
if (b.isExpired()) {
|
||||
bans.remove(uuid, b);
|
||||
saveAsync();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка входа игрока
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -44,11 +44,11 @@ public class AutoMessageManager {
|
|||
|
||||
// Настройки
|
||||
prefix = plugin.getConfigManager().getString("prefix", "�D4FF[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)) {
|
||||
|
|
|
|||
|
|
@ -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<>();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue