Compare commits

...

10 commits

Author SHA1 Message Date
260986ff11
feat: hover translation (Google, Chrome endpoint with gtx fallback), LuckPerms prefixes in chat, restyled messages
Some checks are pending
Build & Test / Build LoChat (push) Waiting to run
2026-09-20 21:05:17 +02:00
bd39118b14
chore: add PMD 7.8.0 and fix warnings/antipatterns
- build.gradle.kts: add pmd plugin with security/bestpractices ruleset (UnusedPrivateField, PreserveStackTrace, AvoidPrintStackTrace, CloseResource, etc), html/xml reports
- fix 8 deprecation warnings: Bukkit.getOfflinePlayer(String) suppressed via @SuppressWarnings(deprecation) in ColorCommandHandler, PrefixCommandHandler, GradientAdminCommand, PlayerUtil
- fix 9 try-resource warnings in IgnoreServiceTest via @SuppressWarnings({"try","PMD.UnusedLocalVariable"})
- PMD fixes: AvoidPrintStackTrace -> logger.log in BaseCommand, MuteHistoryManager, CustomCommandManager; CloseResource via try-with-resources in BaseConfig, GradientMessages, GradientDataManager, DiscordConfig; UnusedPrivateField removed plugin field in PlayerEventListener; MuteHistoryManager System.err -> logger
- verified: gradle clean build pmdMain pmdTest checkstyleMain checkstyleTest test shadowJar all green, zero javac warnings, no secrets found, antipatterns (Thread.sleep in async webhook/rate-limiter kept isolated, printStackTrace removed)
2026-09-20 16:56:02 +02:00
83e980d1bb
trim: moderation → mute only
- remove ban/warn commands: BanCommand (/lban), UnbanCommand (/lunban),
  WarnCommand (/warn), SilentWarnCommand (silentwarn) and empty ban/warn dirs
- plugin.yml: remove commands lban/lunban/warn/silentwarn and
  permissions lochat.ban/lochat.unban/lochat.warn/lochat.warn.silent/
  lochat.warn.notify/lochat.bypass.warn/lochat.bypass.ban/lochat.ban.override;
  remove LibertyBans from softdepend
- remove registration in CommandManager and PluginInitializer
  (LibertyBansHook, ModerationListener)
- delete PunishmentService api+impl, BanRecord/WarnEntry models,
  ModerationListener, LibertyBansHook; strip ServiceRegistry/PluginInitializer/
  LoChat reload and PlayerInfoCommand (warn/ban sections)
- config: remove moderation.default-ban-reason/ban-kick-message from
  config.yml, ban/warn keys from config/messages.yml, escalation header
  Punishment->Mute in mute.yml
- MessagesConfig.java: remove ban/warn getters (keep mute)
- README: remove /lban /warn rows, update features to mute-only
- preserve /lmute /lunmute /lmutelist /lmuteblame, MuteService, mute.yml,
  mutes.json format and HIGHEST chat handler
2026-09-20 16:44:39 +02:00
7f513a7104
port: replace Paper component APIs, chat pipeline, events and schedulers
- Msg.send/broadcast/actionBar/title/legacy/plain for all 58 files sendMessage replacements (plan step 3)
- ChatEventListener: AsyncChatEvent->AsyncPlayerChatEvent, getRecipients filtering, manual render+Msg.send loop, HIGHEST|ignoreCancelled, nobodyHeard via Bukkit scheduler (step 4)
- EnhancedChatRenderer: remove ChatRenderer/Audience, render(Player,Component,Component,Player), getLocale String, Msg.actionBar
- PlayerEventListener: join/quit/death String API, getCustomName/getDisplayName legacy, Msg.broadcast
- FoliaUtil: IS_FOLIA false, Bukkit scheduler only; AutoMessageManager BukkitTask
- CustomCommandManager: reflective getCommandMap; other mechanical fixes (displayName, kick, inventory titles)
- single universal jar (variant A) - loads on Spigot 1.20.1 and Paper
2026-09-20 16:38:37 +02:00
88e4e68cca
port: compat layer
- add compat/Adventure (BukkitAudiences lifecycle with legacy fallback)
- add compat/Msg (send/broadcast/actionBar/title/legacy/plain/displayName/playerListName/kick/itemMeta/inventoryTitle)
- wire Adventure.init/close in LoChat onEnable/onDisable
- remove unused reflection-based utils/platform/AdventureManager
2026-09-20 16:18:40 +02:00
dd725ad255
port: build for spigot 1.20.1 (universal jar)
- replace paper-api with spigot-api 1.20.1, add spigot snapshots repo
- bundle Adventure 4.17.0 + platform-bukkit 4.3.4 via shadowJar with relocate to com.loki.lochat.libs.kyori/gson and mergeServiceFiles
- version 1.5.5-1.20.1, api-version 1.20, remove folia-supported, run-paper
- single universal jar loads on Spigot/Mohist 1.20.1 and Paper 1.20.x-1.21.x (Paper is superset)
- CI: add spigot-1.20.1 branch
2026-09-20 16:18:00 +02:00
a822c91637
feat: chat channel system (/join /leave /channel)
- config-driven channels in config/channels.yml (global, local, staff, trade) with
  emoji, colors, prefix, radius, cooldowns, permissions and aliases
- Channel record + ChannelConfig loader wired into ConfigManager (reload supported)
- ChannelService (api) + ConcurrentHashMap-backed ChannelServiceImpl, joined in
  ServiceRegistry; default channel join treated as 'not in channel'
- JoinChannelCommand / LeaveChannelCommand / ChannelCommand (+ tab completion),
  registered in CommandManager; plugin.yml commands + permissions
- ChatEventListener routes AsyncChatEvent to the active channel: per-channel
  cooldown, local-radius viewer filtering, channel-aware renderer
- EnhancedChatRenderer supports Channel overload (emojiFor/prefixText/channelColor)
- ChannelServiceImplTest covering join/leave/permissions/aliases
2026-08-09 21:31:11 +02:00
689ab1a9ae
feat: anti-swear obfuscation bypass + filter statistics
- SwearNormalizer: expanded leet/lookalike map (q→р, digits), strips separators inside
  words, collapses repeated letters; single source of truth for AhoCorasick
- SwearFilter: detects obfuscated profanity on normalized text, masks tolerant matches
  in replace mode via tolerant word patterns
- FilterStatistics: per-filter block counters -> /lochat filterstats subcommand
- tests: SwearNormalizerTest, FilterStatisticsTest
2026-08-09 21:17:18 +02:00
cda89c78e4
fix: carry-over fixes — per-type cooldowns, PM cooldown, nickname persistence, thread-safe deques
- PlayerServiceImpl: separate cooldown map per chat type (fixes cross-blocking between local chat and RP commands)
- PrivateMessageServiceImpl: enforce chat.pm.cooldown (honors chat.bypass.cooldown)
- NickServiceImpl: persist nicknames async on every change/reset
- FloodFilter/SpamFilter: ConcurrentLinkedDeque instead of ArrayDeque
- tests: PlayerServiceCooldownTest
2026-08-09 21:08:58 +02:00
b1b0c9082b
fix: critical fixes from code review — async translation, atomic persistence, unified chat pipeline, config key alignment 2026-08-09 21:04:07 +02:00
121 changed files with 3039 additions and 2028 deletions

View file

@ -2,7 +2,7 @@ name: Build & Test
on:
push:
branches: [main, develop]
branches: [main, develop, spigot-1.20.1]
pull_request:
branches: [main]

View file

@ -32,7 +32,7 @@ LoChat is a powerful Minecraft chat plugin for Paper 1.19.2+ and Folia servers.
| Private messages | PM system with reply support and ignore list |
| Gradient nicknames | Up to 7 colors with built-in presets (fire, ocean, rainbow) |
| Mentions | @player, @everyone, @here, @role with sounds |
| Moderation | Mute, ban, warn with history, silent mode, escalation |
| Moderation | Mute with history, silent mode, escalation |
| Chat filters | Swear, spam, flood, CAPS, URL/IP, hidden links |
| RP commands | /me, /do, /try, /roll with configurable radius |
| Custom join/quit/death | Messages with weapon name and hover events |
@ -55,8 +55,6 @@ LoChat is a powerful Minecraft chat plugin for Paper 1.19.2+ and Folia servers.
| `/prefix <text>` | `gradient.prefix` | Set custom prefix |
| `/lmute <player> [time] [-s] [reason]` | `lochat.mute` | Mute player |
| `/lunmute <player> [-s]` | `lochat.unmute` | Unmute player |
| `/lban <player> [time] [reason]` | `lochat.ban` | Ban player |
| `/warn <player> [reason]` | `lochat.warn` | Warn player |
| `/chatspy` | `chat.spy` | Toggle spy mode |
| `/clearchat` | `chat.clear` | Clear chat |
| `/lochat reload` | `lochat.admin` | Reload config |
@ -90,7 +88,7 @@ LoChat - мощный чат-плагин для Paper 1.19.2+ и Folia серв
| Личные сообщения | PM с ответом и списком игнора |
| Градиентные ники | До 7 цветов с пресетами (огонь, океан, радуга) |
| Упоминания | @player, @everyone, @here, @role со звуками |
| Модерация | Мут, бан, варн с историей, тихий режим, эскалация |
| Модерация | Мут с историей, тихий режим, эскалация |
| Фильтры чата | Мат, спам, флуд, CAPS, URL/IP, скрытые ссылки |
| RP команды | /me, /do, /try, /roll с настраиваемым радиусом |
| Кастомные вход/выход/смерть | Сообщения с названием оружия и hover-событиями |
@ -109,8 +107,6 @@ LoChat - мощный чат-плагин для Paper 1.19.2+ и Folia серв
| `/msg <игрок> <сообщение>` | `chat.pm.use` | Личное сообщение |
| `/color <hex1> [hex2]` | `gradient.color` | Установить градиент |
| `/lmute <игрок> [время] [-s] [причина]` | `lochat.mute` | Замутить |
| `/lban <игрок> [время] [причина]` | `lochat.ban` | Забанить |
| `/warn <игрок> [причина]` | `lochat.warn` | Выдать варн |
| `/lochat reload` | `lochat.admin` | Перезагрузить конфиг |
### Зависимости

91
TODO.md
View file

@ -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,35 @@
---
## 🟡 REMAINING (COULD DO)
## 🟡 NEXT: NEW FEATURES
### 1. Channels (like VentureChat / Carbon) ✅
- `/join`, `/leave`, `/channel` — full channel system: global, local, staff, trade.
- Config-driven channels via `config/channels.yml` (emoji, colors, prefix, radius, cooldowns, permissions, aliases).
- Chat routing: `ChatEventListener` sends to the joined channel; local radius filtering; per-channel cooldown; channel-aware `EnhancedChatRenderer`.
- Party chat: private groups `/party` (future).
- Network channels (Bungee/Velocity) sync (future).
### 2. Anti-swear hardening + filter statistics ✅
- Obfuscation bypass: `SwearNormalizer` (q→р / leet-подстановки, повторы букв «сууука», разделители внутри слова «с.у.к.а» / «с у к а») + tolerant-маскирование в replace-режиме. Single source of truth для Aho-Corasick.
- Filter stats: `FilterStatistics` считает блокировки по каждому фильтру → `/lochat filterstats`.
### 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,29 +100,26 @@
**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` now saves asynchronously (`FoliaUtil.runAsync → save()`) on every `setNickname`/`resetNickname` — nicknames survive crashes.
### 3. DIP: Service Locator Anti-Pattern
`ServiceRegistry.get(Xxx.class)` used everywhere instead of constructor DI. Big refactor.
### 4. DRY: PM Send Logic Duplicated
`MsgCommand` + `ReplyCommand` still have near-identical send logic.
### 4. DRY: PM Send Logic ✅
PM cooldown + actual send centralized in `PrivateMessageServiceImpl.sendPrivateMessage`; both `/msg` and `/reply` delegate to it.
### 5. PM Send Logic Duplicated Between MsgCommand and ReplyCommand
### 5. Fix cooldown map collision ✅
`PlayerServiceImpl` now keeps a separate `ConcurrentHashMap` per chat type (`global`, `local`, `rp_me`, `rp_do`, `rp_try`, `pm`) — no more cross-blocking. Covered by `PlayerServiceCooldownTest`.
---
### 6. Thread-safe flood/spam deques ✅
`FloodFilter`/`SpamFilter` now use `ConcurrentLinkedDeque` instead of plain `ArrayDeque`.
## 🔵 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` (default 2s) is now enforced in `PrivateMessageServiceImpl.sendPrivateMessage`, honoring `chat.bypass.cooldown`.
---

View file

@ -1,16 +1,21 @@
plugins {
java
id("xyz.jpenilla.run-paper") version "2.3.1"
id("com.gradleup.shadow") version "9.0.0"
jacoco
checkstyle
pmd
}
group = "com.loki"
version = "1.5.5-1.21.x"
// Universal jar: compiled against Spigot 1.20.1 (lowest common denominator).
// Single jar loads on both Spigot/Mohist 1.20.1 and Paper 1.20.x-1.21.x
// because Paper is a superset of Spigot API. Adventure is bundled & relocated.
version = "1.5.5-1.20.1"
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
maven("https://repo.rosewooddev.io/repository/public/")
maven("https://jitpack.io")
@ -18,16 +23,23 @@ repositories {
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
compileOnly("org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT")
compileOnly("me.clip:placeholderapi:2.11.6")
compileOnly("org.black_ixx:playerpoints:3.2.7")
compileOnly("net.luckperms:api:5.4")
implementation("com.google.code.gson:gson:2.10.1")
// Adventure bundled & relocated -> works on Spigot (no server Adventure) and Paper (relocated avoids clash)
implementation("net.kyori:adventure-api:4.17.0")
implementation("net.kyori:adventure-text-minimessage:4.17.0")
implementation("net.kyori:adventure-text-serializer-legacy:4.17.0")
implementation("net.kyori:adventure-text-serializer-plain:4.17.0")
implementation("net.kyori:adventure-text-serializer-gson:4.17.0")
implementation("net.kyori:adventure-platform-bukkit:4.3.4")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0")
testImplementation("org.mockito:mockito-core:5.11.0")
testImplementation("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
testImplementation("org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0")
}
@ -46,6 +58,37 @@ tasks.withType<Checkstyle>().configureEach {
}
}
pmd {
toolVersion = "7.8.0"
ruleSetConfig = resources.text.fromString(
"""
<?xml version="1.0"?>
<ruleset name="LoChat PMD"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>LoChat security + best practices (no style nit)</description>
<rule ref="category/java/bestpractices.xml/UnusedPrivateField"/>
<rule ref="category/java/bestpractices.xml/UnusedLocalVariable"/>
<rule ref="category/java/bestpractices.xml/PreserveStackTrace"/>
<rule ref="category/java/bestpractices.xml/AvoidPrintStackTrace"/>
<rule ref="category/java/bestpractices.xml/UseCollectionIsEmpty"/>
<rule ref="category/java/errorprone.xml/CloseResource"/>
<rule ref="category/java/errorprone.xml/BrokenNullCheck"/>
<rule ref="category/java/security.xml/HardCodedCryptoKey"/>
</ruleset>
""".trimIndent()
)
isIgnoreFailures = false
}
tasks.withType<org.gradle.api.plugins.quality.Pmd>().configureEach {
reports {
xml.required.set(true)
html.required.set(true)
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
@ -79,12 +122,19 @@ jacoco {
}
tasks.jar {
archiveClassifier.set("plain")
archiveFileName.set("LoChat-${project.version}.jar")
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
// Локальный Paper для тестов: .\gradlew.bat runServer
tasks.runServer {
minecraftVersion("1.21.4") // lowest supported; runs on any 1.21.x Paper server
tasks.shadowJar {
archiveClassifier.set("")
archiveFileName.set("LoChat-${project.version}.jar")
mergeServiceFiles()
relocate("net.kyori", "com.loki.lochat.libs.kyori")
relocate("com.google.gson", "com.loki.lochat.libs.gson")
// do NOT minimize(): strips ServiceLoader classes Adventure needs
}
tasks.build {
dependsOn(tasks.shadowJar)
}

View file

@ -4,7 +4,6 @@ import com.loki.lochat.api.service.MessageService;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.commands.CommandManager;
import com.loki.lochat.config.ConfigManager;
import com.loki.lochat.config.MessageConfig;
@ -42,11 +41,13 @@ public final class LoChat extends JavaPlugin {
// Вспомогательные классы
private PluginInitializer initializer;
private PluginShutdown shutdown;
private com.loki.lochat.core.filter.AdvancedMessageFilter chatFilter;
@Override
public void onEnable() {
instance = this;
getLogger().info("Загрузка LoChat v" + getPluginMeta().getVersion() + " для Paper/Folia " + Bukkit.getMinecraftVersion() + "...");
com.loki.lochat.compat.Adventure.init(this);
getLogger().info("Загрузка LoChat v" + getDescription().getVersion() + " для Spigot " + Bukkit.getBukkitVersion() + "...");
// Инициализация
initializer = new PluginInitializer(this);
@ -59,6 +60,8 @@ public final class LoChat extends JavaPlugin {
configManager.getTranslationEndpoint(),
configManager.getTranslationApiKey(),
configManager.isTranslationEnabled(),
configManager.getTranslationProvider(),
configManager.getTranslationTimeoutMs(),
getLogger()
);
@ -81,8 +84,7 @@ public final class LoChat extends JavaPlugin {
serviceRegistry.get(MessageService.class),
serviceRegistry.get(PlayerService.class),
serviceRegistry.get(MessagingService.class),
serviceRegistry.get(NickService.class),
serviceRegistry.get(PunishmentService.class));
serviceRegistry.get(NickService.class));
initializer.registerPluginChannels();
// Автосообщения
@ -94,6 +96,7 @@ public final class LoChat extends JavaPlugin {
@Override
public void onDisable() {
shutdown.shutdown(serviceRegistry, autoMessageManager, discordIntegration, gradientModule);
com.loki.lochat.compat.Adventure.close();
}
public void reload() {
@ -109,12 +112,6 @@ public final class LoChat extends JavaPlugin {
if (discordIntegration != null) {
discordIntegration.reload();
}
if (serviceRegistry != null) {
try {
serviceRegistry.get(PunishmentService.class).save();
} catch (IllegalStateException ignored) {
}
}
}
// Геттеры
@ -153,4 +150,12 @@ public final class LoChat extends JavaPlugin {
public TranslationService getTranslationService() {
return translationService;
}
public com.loki.lochat.core.filter.AdvancedMessageFilter getChatFilter() {
return chatFilter;
}
public void setChatFilter(com.loki.lochat.core.filter.AdvancedMessageFilter chatFilter) {
this.chatFilter = chatFilter;
}
}

View file

@ -0,0 +1,40 @@
package com.loki.lochat.api.service;
import com.loki.lochat.channel.Channel;
import org.bukkit.entity.Player;
import java.util.Collection;
import java.util.Optional;
/**
* Сервис каналов чата: состояние игроков (в каком канале они состоят).
*/
public interface ChannelService {
boolean isEnabled();
Channel getDefault();
Collection<Channel> getChannels();
Channel find(String nameOrAlias);
/**
* Активный (не-дефолтный) канал игрока, если он его присоединил через /join.
*/
Optional<Channel> getActive(Player player);
boolean isInChannel(Player player);
boolean canUse(Player player, Channel channel);
/**
* Переводит игрока в канал. Дефолтный канал трактуется как «не присоединён».
*
* @return true если присоединение выполнено, false если нет канала или нет прав
*/
boolean join(Player player, Channel channel);
void leave(Player player);
}

View file

@ -1,39 +0,0 @@
package com.loki.lochat.api.service;
import com.loki.lochat.data.model.BanRecord;
import com.loki.lochat.data.model.WarnEntry;
import net.kyori.adventure.text.Component;
import java.util.List;
import java.util.UUID;
/**
* Варны и баны LoChat
*/
public interface PunishmentService {
void addWarn(UUID uuid, String playerName, String moderator, String reason, boolean silent);
List<WarnEntry> getWarns(UUID uuid);
int getWarnCount(UUID uuid);
/**
* @param durationMs длительность; 0 = навсегда
*/
void ban(UUID uuid, String playerName, long durationMs, String reason, String bannedBy);
boolean unban(UUID uuid);
boolean isBanned(UUID uuid);
BanRecord getActiveBan(UUID uuid);
/** Сообщение при кике (MiniMessage + legacy &#) */
Component buildBanKickMessage(BanRecord ban);
void save();
void reload();
}

View file

@ -0,0 +1,33 @@
package com.loki.lochat.channel;
import java.util.List;
/**
* Канал чата (global, local, staff, trade...).
* Иммутабельное описание канала из config/channels.yml.
*/
public record Channel(
String id,
String display,
String prefix,
String color,
int radius,
int cooldown,
String permission,
List<String> aliases) {
/** radius &lt; 1 — глобальный канал (видны всем игрокам) */
public boolean isGlobalScope() {
return radius < 1;
}
/** radius &gt; 0 — локальный канал с радиусом действия */
public boolean isLocalScope() {
return radius > 0;
}
/** Есть ли требование по праву для входа/использования */
public boolean hasPermission() {
return permission != null && !permission.isBlank();
}
}

View file

@ -8,22 +8,21 @@ import com.loki.lochat.commands.admin.chat.ClearChatCommand;
import com.loki.lochat.commands.admin.chat.ClearChatConfigCommand;
import com.loki.lochat.commands.admin.system.LoChatCommand;
import com.loki.lochat.commands.admin.system.ReloadConfigCommand;
import com.loki.lochat.commands.chat.ChannelCommand;
import com.loki.lochat.commands.chat.GlobalChatCommand;
import com.loki.lochat.commands.chat.JoinChannelCommand;
import com.loki.lochat.commands.chat.LeaveChannelCommand;
import com.loki.lochat.commands.chat.LocalChatCommand;
import com.loki.lochat.commands.messaging.IgnoreCommand;
import com.loki.lochat.commands.messaging.IgnoreListCommand;
import com.loki.lochat.commands.messaging.MsgCommand;
import com.loki.lochat.commands.messaging.ReplyCommand;
import com.loki.lochat.commands.messaging.UnignoreCommand;
import com.loki.lochat.commands.moderation.ban.BanCommand;
import com.loki.lochat.commands.moderation.ban.UnbanCommand;
import com.loki.lochat.commands.moderation.mute.MuteBlameCommand;
import com.loki.lochat.commands.moderation.mute.MuteCommand;
import com.loki.lochat.commands.moderation.mute.MuteHistoryCommand;
import com.loki.lochat.commands.moderation.mute.MuteListCommand;
import com.loki.lochat.commands.moderation.mute.UnmuteCommand;
import com.loki.lochat.commands.moderation.warn.SilentWarnCommand;
import com.loki.lochat.commands.moderation.warn.WarnCommand;
import com.loki.lochat.commands.nick.NickCommand;
import com.loki.lochat.commands.nick.PlayerInfoCommand;
import com.loki.lochat.commands.rp.DoCommand;
@ -54,6 +53,11 @@ public class CommandManager {
reg("g", new GlobalChatCommand(plugin));
reg("l", new LocalChatCommand(plugin));
// ── Каналы ─────────────────────────────────────────────────────────────
regTab("join", new JoinChannelCommand(plugin));
reg("leave", new LeaveChannelCommand(plugin));
regTab("channel", new ChannelCommand(plugin));
// ── Личные сообщения / игнор ──────────────────────────────────────────
reg("msg", new MsgCommand(plugin));
reg("reply", new ReplyCommand(plugin));
@ -67,10 +71,6 @@ public class CommandManager {
reg("lmutelist", new MuteListCommand(plugin));
regTab("lmutehistory", new MuteHistoryCommand(plugin));
regTab("lmuteblame", new MuteBlameCommand(plugin));
regTab("warn", new WarnCommand(plugin));
regTab("silentwarn", new SilentWarnCommand(plugin));
regTab("lban", new BanCommand(plugin));
regTab("lunban", new UnbanCommand(plugin));
// ── Ник / профиль ─────────────────────────────────────────────────────
regTab("nick", new NickCommand(plugin));

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.managers.CustomCommandManager;
import com.loki.lochat.utils.format.ChatFormatter;
@ -28,7 +30,7 @@ public class CustomCommandsCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission("lochat.customcommands")) {
sender.sendMessage(ChatFormatter.parse("<#F44336>❌ У вас нет прав для использования этой команды!"));
Msg.send(sender, ChatFormatter.parse("<#F44336>❌ У вас нет прав для использования этой команды!"));
return true;
}
@ -42,14 +44,14 @@ public class CustomCommandsCommand implements CommandExecutor, TabCompleter {
case "reload" -> reloadCommands(sender);
case "enable" -> {
if (args.length < 2) {
sender.sendMessage(ChatFormatter.parse("<#FFA726>⚠️ Использование: /customcommands enable <команда>"));
Msg.send(sender, ChatFormatter.parse("<#FFA726>⚠️ Использование: /customcommands enable <команда>"));
return true;
}
toggleCommand(sender, args[1], true);
}
case "disable" -> {
if (args.length < 2) {
sender.sendMessage(ChatFormatter.parse("<#FFA726>⚠️ Использование: /customcommands disable <команда>"));
Msg.send(sender, ChatFormatter.parse("<#FFA726>⚠️ Использование: /customcommands disable <команда>"));
return true;
}
toggleCommand(sender, args[1], false);
@ -73,7 +75,7 @@ public class CustomCommandsCommand implements CommandExecutor, TabCompleter {
<gradient:#FFD700:#FFA500><bold>▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</bold></gradient>
""");
sender.sendMessage(message);
Msg.send(sender, message);
}
private void showCommandsList(CommandSender sender) {
@ -91,21 +93,21 @@ public class CustomCommandsCommand implements CommandExecutor, TabCompleter {
}
message.append(ChatFormatter.parse("\n<gradient:#FFD700:#FFA500><bold>▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</bold></gradient>"));
sender.sendMessage(message.build());
Msg.send(sender, message.build());
}
private void reloadCommands(CommandSender sender) {
try {
commandManager.reload();
sender.sendMessage(ChatFormatter.parse("<#4CAF50>✅ Кастомные команды успешно перезагружены!"));
Msg.send(sender, ChatFormatter.parse("<#4CAF50>✅ Кастомные команды успешно перезагружены!"));
} catch (Exception e) {
sender.sendMessage(ChatFormatter.parse("<#F44336>❌ Ошибка при перезагрузке команд: " + e.getMessage()));
Msg.send(sender, ChatFormatter.parse("<#F44336>❌ Ошибка при перезагрузке команд: " + e.getMessage()));
plugin.getLogger().severe("Ошибка перезагрузки кастомных команд: " + e.getMessage());
}
}
private void toggleCommand(CommandSender sender, String commandName, boolean enable) {
sender.sendMessage(ChatFormatter.parse(
Msg.send(sender, ChatFormatter.parse(
"<#FFA726>⚠️ Для изменения статуса команд отредактируйте файл custom-commands.yml и выполните /customcommands reload"
));
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.admin.broadcast;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.format.ChatFormatter;
@ -26,12 +28,12 @@ public class AnnounceCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (args.length == 0) {
sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/announce <сообщение>")));
Msg.send(sender, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/announce <сообщение>")));
return true;
}
if (args[0].equalsIgnoreCase("reload") && sender.hasPermission("chat.announce.reload")) {
plugin.reload();
sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("announce.reloaded")));
Msg.send(sender, ChatFormatter.parse(plugin.getMessageConfig().get("announce.reloaded")));
return true;
}
@ -39,17 +41,17 @@ public class AnnounceCommand implements CommandExecutor {
Component formatted = ChatFormatter.formatAnnouncement(plugin.getConfigManager().getAnnouncementFormat(), message);
for (Player player : Bukkit.getOnlinePlayers()) {
player.sendMessage(formatted);
Msg.send(player, formatted);
if (plugin.getConfigManager().isAnnouncementActionBarEnabled()) {
player.sendActionBar(formatted);
Msg.actionBar(player, formatted);
}
if (plugin.getConfigManager().isAnnouncementTitleEnabled()) {
player.showTitle(Title.title(
Msg.title(player,
ChatFormatter.parse(plugin.getConfigManager().getAnnouncementTitleHeader()),
ChatFormatter.parse(message),
Title.Times.times(Duration.ofMillis(500),
Duration.ofSeconds(plugin.getConfigManager().getAnnouncementTitleDuration()),
Duration.ofMillis(500))));
Duration.ofMillis(500)));
}
}
plugin.getLogger().info("[ANNOUNCE] " + sender.getName() + ": " + ChatFormatter.stripTags(message));

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.admin.broadcast;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.integrations.DiscordIntegration;
import com.loki.lochat.utils.format.ChatFormatter;
@ -27,7 +29,7 @@ public class DiscordCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.discord.admin")) {
sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("errors.no-permission")));
Msg.send(sender, ChatFormatter.parse(plugin.getMessageConfig().get("errors.no-permission")));
return true;
}
if (args.length == 0) {
@ -35,29 +37,30 @@ public class DiscordCommand implements CommandExecutor, TabCompleter {
return true;
}
switch (args[0].toLowerCase()) {
case "status" -> sender.sendMessage(ChatFormatter.parse(
case "status" -> Msg.send(sender, ChatFormatter.parse(
discord.isEnabled() ? "&#9878C9Discord &#7858E9включён" : "&#9878C9Discord &#CF6679отключён"));
case "reload" -> {
try {
discord.reload();
sender.sendMessage(ChatFormatter.parse("&#9878C9Discord &#7858E9перезагружен"));
Msg.send(sender, ChatFormatter.parse("&#9878C9Discord &#7858E9перезагружен"));
} catch (Exception e) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Ошибка: " + e.getMessage()));
Msg.send(sender, ChatFormatter.parse("&#CF6679Ошибка: " + e.getMessage()));
}
}
case "test" -> {
if (!discord.isEnabled()) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Discord отключён"));
Msg.send(sender, ChatFormatter.parse("&#CF6679Discord отключён"));
return true;
}
String msg = args.length > 1 ? String.join(" ", Arrays.copyOfRange(args, 1, args.length)) : "Тест от " + sender.getName();
sender.sendMessage(ChatFormatter.parse("&#B798A8Отправка..."));
Msg.send(sender, ChatFormatter.parse("&#B798A8Отправка..."));
java.util.concurrent.CompletableFuture.runAsync(() -> {
discord.sendTestMessage(msg, sender.getName());
if (sender instanceof org.bukkit.entity.Player p) {
p.getScheduler().run(plugin, t -> sender.sendMessage(ChatFormatter.parse("&#9878C9Отправлено &#7858E9✓")), null);
if (sender instanceof org.bukkit.entity.Player) {
org.bukkit.Bukkit.getScheduler().runTask(plugin,
() -> Msg.send(sender, ChatFormatter.parse("&#9878C9Отправлено &#7858E9✓")));
} else {
sender.sendMessage(ChatFormatter.parse("&#9878C9Отправлено &#7858E9✓"));
Msg.send(sender, ChatFormatter.parse("&#9878C9Отправлено &#7858E9✓"));
}
});
}
@ -67,7 +70,7 @@ public class DiscordCommand implements CommandExecutor, TabCompleter {
}
private void sendHelp(CommandSender s) {
s.sendMessage(ChatFormatter.parse("&#B798A8/discordadmin status|reload|test"));
Msg.send(s, ChatFormatter.parse("&#B798A8/discordadmin status|reload|test"));
}
@Override

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.admin.chat;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.utils.format.ChatFormatter;
@ -27,7 +29,7 @@ public class ChatSpyCommand implements CommandExecutor {
return true;
}
boolean enabled = messagingService.toggleSpy(player.getUniqueId());
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get(enabled ? "spy.enabled" : "spy.disabled")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get(enabled ? "spy.enabled" : "spy.disabled")));
return true;
}
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.admin.chat;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.format.ChatFormatter;
@ -26,7 +28,7 @@ public class ClearChatCommand implements CommandExecutor {
player.sendMessage("");
}
if (plugin.getConfigManager().isClearChatMessageEnabled()) {
player.sendMessage(ChatFormatter.parse(
Msg.send(player, ChatFormatter.parse(
plugin.getConfigManager().getClearChatMessage().replace("{player}", sender.getName())));
}
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.admin.chat;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.format.ChatFormatter;
@ -24,37 +26,37 @@ public class ClearChatConfigCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.clearchat.config")) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission"));
Msg.send(sender, plugin.getMessageConfig().getComponent("errors.no-permission"));
return true;
}
if (args.length == 0) {
sender.sendMessage(ChatFormatter.parse("&#B798A8Использование: &#7858E9/clearchatconfig <enable|disable|message|status>"));
Msg.send(sender, ChatFormatter.parse("&#B798A8Использование: &#7858E9/clearchatconfig <enable|disable|message|status>"));
return true;
}
switch (args[0].toLowerCase()) {
case "enable" -> {
plugin.getConfigManager().setClearChatMessageEnabled(true);
sender.sendMessage(ChatFormatter.parse("&#9878C9Сообщение очистки &#7858E9включено"));
Msg.send(sender, ChatFormatter.parse("&#9878C9Сообщение очистки &#7858E9включено"));
}
case "disable" -> {
plugin.getConfigManager().setClearChatMessageEnabled(false);
sender.sendMessage(ChatFormatter.parse("&#9878C9Сообщение очистки &#B798A8отключено"));
Msg.send(sender, ChatFormatter.parse("&#9878C9Сообщение очистки &#B798A8отключено"));
}
case "message" -> {
if (args.length < 2) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Укажите текст!"));
Msg.send(sender, ChatFormatter.parse("&#CF6679Укажите текст!"));
return true;
}
String msg = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
plugin.getConfigManager().setClearChatMessage(msg);
sender.sendMessage(ChatFormatter.parse("&#9878C9Сообщение установлено: &f" + msg));
Msg.send(sender, ChatFormatter.parse("&#9878C9Сообщение установлено: &f" + msg));
}
case "status" -> {
sender.sendMessage(ChatFormatter.parse("&#B798A8Включено: "
Msg.send(sender, ChatFormatter.parse("&#B798A8Включено: "
+ (plugin.getConfigManager().isClearChatMessageEnabled() ? "&#7858E9да" : "&#CF6679нет")));
sender.sendMessage(ChatFormatter.parse("&#B798A8Текст: &f" + plugin.getConfigManager().getClearChatMessage()));
Msg.send(sender, ChatFormatter.parse("&#B798A8Текст: &f" + plugin.getConfigManager().getClearChatMessage()));
}
default -> sender.sendMessage(ChatFormatter.parse("&#CF6679Неизвестный аргумент"));
default -> Msg.send(sender, ChatFormatter.parse("&#CF6679Неизвестный аргумент"));
}
return true;
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.admin.system;
import com.loki.lochat.compat.Msg;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import com.loki.lochat.LoChat;
@ -21,11 +23,11 @@ public class HubCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("general.players-only")));
Msg.send(sender, ChatFormatter.parse(plugin.getMessageConfig().get("general.players-only")));
return true;
}
if (!plugin.getConfigManager().getBoolean("hub_command.enabled", true)) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("general.command-disabled")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("general.command-disabled")));
return true;
}
String server = plugin.getConfigManager().getString("hub_command.server", "hub");
@ -33,7 +35,7 @@ public class HubCommand implements CommandExecutor {
out.writeUTF("Connect");
out.writeUTF(server);
player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray());
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("hub.connecting").replace("%server%", server)));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("hub.connecting").replace("%server%", server)));
return true;
}
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.admin.system;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.format.ChatFormatter;
@ -24,32 +26,48 @@ public class LoChatCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.admin")) {
sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getNoPermission()));
Msg.send(sender, ChatFormatter.parse(plugin.getMessageConfig().getNoPermission()));
return true;
}
if (args.length == 0) {
sender.sendMessage(ChatFormatter.parse("&#7858E9LoChat &#B798A8v" + plugin.getPluginMeta().getVersion()));
sender.sendMessage(ChatFormatter.parse("&#9878C9/lochat reload &#B798A8— перезагрузить конфиги"));
sender.sendMessage(ChatFormatter.parse("&#9878C9/lochat commands [reload] &#B798A8— кастомные команды"));
Msg.send(sender, ChatFormatter.parse("&#7858E9LoChat &#B798A8v" + plugin.getDescription().getVersion()));
Msg.send(sender, ChatFormatter.parse("&#9878C9/lochat reload &#B798A8— перезагрузить конфиги"));
Msg.send(sender, ChatFormatter.parse("&#9878C9/lochat commands [reload] &#B798A8— кастомные команды"));
Msg.send(sender, ChatFormatter.parse("&#9878C9/lochat filterstats &#B798A8— статистика фильтров чата"));
return true;
}
switch (args[0].toLowerCase()) {
case "reload" -> {
plugin.reload();
sender.sendMessage(ChatFormatter.parse("&#9878C9Конфигурация &#7858E9перезагружена!"));
Msg.send(sender, ChatFormatter.parse("&#9878C9Конфигурация &#7858E9перезагружена!"));
}
case "commands" -> {
if (args.length > 1 && args[1].equalsIgnoreCase("reload")) {
plugin.getCustomCommandManager().reload();
sender.sendMessage(ChatFormatter.parse("&#9878C9Кастомные команды &#7858E9перезагружены!"));
Msg.send(sender, ChatFormatter.parse("&#9878C9Кастомные команды &#7858E9перезагружены!"));
} else {
var cmds = plugin.getCustomCommandManager().getCommands();
sender.sendMessage(ChatFormatter.parse("&#B798A8Кастомные команды &#9878C9(" + cmds.size() + "&#9878C9):"));
cmds.values().forEach(c -> sender.sendMessage(
ChatFormatter.parse("&#7858E9/" + c.name() + " &#B798A8(" + c.type() + ")")));
Msg.send(sender, ChatFormatter.parse("&#B798A8Кастомные команды &#9878C9(" + cmds.size() + "&#9878C9):"));
cmds.values().forEach(c -> Msg.send(sender, ChatFormatter.parse("&#7858E9/" + c.name() + " &#B798A8(" + c.type() + ")")));
}
}
default -> sender.sendMessage(ChatFormatter.parse("&#CF6679Неизвестная команда"));
case "filterstats" -> {
var filter = plugin.getChatFilter();
if (filter == null) {
Msg.send(sender, ChatFormatter.parse("&#CF6679Фильтр чата недоступен"));
break;
}
var stats = filter.getStatistics();
long total = stats.getTotalBlocked();
Msg.send(sender, ChatFormatter.parse(
"&#B798A8Фильтры чата: &#9878C9заблокировано &#7858E9" + total + "&#9878C9 сообщений"));
if (total == 0) {
Msg.send(sender, ChatFormatter.parse("&#B798A8Пока нечего показывать"));
break;
}
stats.snapshot().forEach((name, count) -> Msg.send(sender, ChatFormatter.parse("&#7858E9" + name + ":&#9878C9 " + count)));
}
default -> Msg.send(sender, ChatFormatter.parse("&#CF6679Неизвестная команда"));
}
return true;
}
@ -63,7 +81,8 @@ public class LoChatCommand implements CommandExecutor, TabCompleter {
return new ArrayList<>();
}
if (args.length == 1) {
return List.of("reload", "commands").stream().filter(s -> s.startsWith(args[0].toLowerCase())).toList();
return List.of("reload", "commands", "filterstats").stream()
.filter(s -> s.startsWith(args[0].toLowerCase())).toList();
}
if (args.length == 2 && args[0].equalsIgnoreCase("commands")) {
return List.of("reload");

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.admin.system;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.format.ChatFormatter;
@ -19,14 +21,14 @@ public class ReloadConfigCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.admin.reload")) {
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission()));
Msg.send(sender, ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission()));
return true;
}
try {
plugin.reload();
sender.sendMessage(ChatFormatter.parse("&#9878C9Все конфигурации &#7858E9успешно перезагружены!"));
Msg.send(sender, ChatFormatter.parse("&#9878C9Все конфигурации &#7858E9успешно перезагружены!"));
} catch (Exception e) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Ошибка при перезагрузке: " + e.getMessage()));
Msg.send(sender, ChatFormatter.parse("&#CF6679Ошибка при перезагрузке: " + e.getMessage()));
plugin.getLogger().severe("Ошибка при перезагрузке: " + e.getMessage());
}
return true;

View file

@ -11,6 +11,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.logging.Level;
/**
* Базовый класс для всех команд плагина
@ -30,8 +31,7 @@ public abstract class BaseCommand implements CommandExecutor, TabCompleter {
try {
return executeCommand(sender, command, label, args);
} catch (Exception e) {
plugin.getLogger().severe("Ошибка в команде " + command.getName() + ": " + e.getMessage());
e.printStackTrace();
plugin.getLogger().log(Level.WARNING, "Ошибка в команде " + command.getName() + ": " + e.getMessage(), e);
sender.sendMessage("§cПроизошла ошибка при выполнении команды!");
return true;
}

View file

@ -0,0 +1,122 @@
package com.loki.lochat.commands.chat;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChannelService;
import com.loki.lochat.channel.Channel;
import com.loki.lochat.commands.base.PlayerCommand;
import com.loki.lochat.utils.format.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Locale;
/**
* /channel — инфо о текущем канале со списком доступных.
* /channel &lt;канал&gt; — переключиться. /channel off — выйти.
* /channel list — список каналов.
*/
public class ChannelCommand extends PlayerCommand {
public ChannelCommand(LoChat plugin) {
super(plugin);
}
@Override
protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class);
if (!channels.isEnabled()) {
Msg.send(player, ChatFormatter.parse("&#CF6679Система каналов отключена"));
return true;
}
if (args.length == 0) {
showCurrent(player, channels);
return true;
}
switch (args[0].toLowerCase(Locale.ROOT)) {
case "off", "leave" -> {
if (channels.isInChannel(player)) {
channels.leave(player);
Msg.send(player, ChatFormatter.parse("&#9878C9Вы вышли из канала"));
} else {
Msg.send(player, ChatFormatter.parse("&#B798A8Вы не находитесь в канале"));
}
}
case "list" -> listChannels(player, channels);
default -> joinChannel(player, channels, args[0]);
}
return true;
}
private void showCurrent(Player player, ChannelService channels) {
Channel active = channels.getActive(player).orElse(null);
if (active == null) {
Channel def = channels.getDefault();
Msg.send(player, ChatFormatter.parse("&#B798A8Вы в дефолтном канале [&#7858E9"
+ (def == null ? "?" : def.display() + "&8 " + def.prefix()) + "&#B798A8]"));
} else {
Msg.send(player, ChatFormatter.parse("&#9878C9Вы в канале [&#7858E9"
+ active.display() + "&8 " + active.prefix() + "&#9878C9]"));
}
Msg.send(player, ChatFormatter.parse("&#B798A8/join <канал> &#9878C9— войти, "
+ "&#B798A8/channel list &#9878C9— все каналы"));
}
private void listChannels(Player player, ChannelService channels) {
Msg.send(player, ChatFormatter.parse("&#B798A8Доступные каналы:"));
for (Channel c : channels.getChannels()) {
boolean canUse = channels.canUse(player, c);
boolean active = c.equals(channels.getActive(player).orElse(null));
Msg.send(player, ChatFormatter.parse((canUse ? "&#9878C9" : "&#CF6679")
+ (active ? "\u25B8 " : "\u25AA ") + c.display() + "&8 " + c.prefix()
+ (canUse ? "" : " &#CF6679(нет прав)")
+ " &#B798A8— " + (c.isGlobalScope() ? "глобальный" : "локальный, " + c.radius() + "б")));
}
}
private void joinChannel(Player player, ChannelService channels, String name) {
Channel channel = channels.find(name);
if (channel == null) {
Msg.send(player, ChatFormatter.parse("&#CF6679Канал &#7858E9" + name + " &#CF6679не найден"));
return;
}
if (!channels.canUse(player, channel)) {
Msg.send(player, ChatFormatter.parse(
plugin.getConfigManager().getMessagesConfig().getNoPermission()));
return;
}
Channel active = channels.getActive(player).orElse(null);
channels.join(player, channel);
if (channel.equals(active)) {
Msg.send(player, ChatFormatter.parse("&#B798A8Вы уже в канале [&#7858E9"
+ channel.display() + "&8 " + channel.prefix() + "&#B798A8]"));
} else {
Msg.send(player, ChatFormatter.parse("&#9878C9Вы вошли в канал [&#7858E9"
+ channel.display() + "&8 " + channel.prefix() + "&#9878C9]"));
}
}
@Override
protected @Nullable List<String> getTabCompletions(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
if (args.length != 1) {
return null;
}
ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class);
String input = args[0].toLowerCase(Locale.ROOT);
return java.util.stream.Stream.concat(
java.util.stream.Stream.of("list", "off"),
channels.getChannels().stream().map(Channel::id))
.filter(s -> s.startsWith(input))
.toList();
}
}

View file

@ -1,11 +1,11 @@
package com.loki.lochat.commands.chat;
import com.loki.lochat.compat.Msg;
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 +14,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;
@ -33,20 +32,20 @@ public class GlobalChatCommand implements CommandExecutor {
if (command.getName().equalsIgnoreCase("globalchat")) {
if (args.length > 0 && args[0].equalsIgnoreCase("toggle")) {
boolean enabled = chatService.toggleGlobalChat(player.getUniqueId());
player.sendMessage(ChatFormatter.parse(
Msg.send(player, ChatFormatter.parse(
plugin.getMessageConfig().get(enabled ? "global.toggled-on" : "global.toggled-off")));
} else {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/globalchat toggle")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/globalchat toggle")));
}
return true;
}
if (args.length == 0) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/g <сообщение>")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/g <сообщение>")));
return true;
}
chatService.sendGlobalMessage(player, MM.deserialize(String.join(" ", args)));
chatService.sendGlobalMessage(player, String.join(" ", args));
return true;
}
}

View file

@ -0,0 +1,82 @@
package com.loki.lochat.commands.chat;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChannelService;
import com.loki.lochat.channel.Channel;
import com.loki.lochat.commands.base.PlayerCommand;
import com.loki.lochat.utils.format.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Locale;
/**
* /join &lt;канал|алиас&gt; — войти в канал чата.
*/
public class JoinChannelCommand extends PlayerCommand {
public JoinChannelCommand(LoChat plugin) {
super(plugin);
}
@Override
protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!plugin.getConfigManager().getChannelConfig().isEnabled()) {
Msg.send(player, ChatFormatter.parse("&#CF6679Система каналов отключена"));
return true;
}
if (args.length == 0) {
Msg.send(player, ChatFormatter.parse("&#CF6679Использование: &#7858E9/join <канал>"));
return true;
}
ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class);
Channel channel = channels.find(args[0]);
if (channel == null) {
Msg.send(player, ChatFormatter.parse("&#CF6679Канал &#7858E9" + args[0] + " &#CF6679не найден"));
return true;
}
if (!channels.canUse(player, channel)) {
Msg.send(player, ChatFormatter.parse(
plugin.getConfigManager().getMessagesConfig().getNoPermission()));
return true;
}
Channel active = channels.getActive(player).orElse(null);
channels.join(player, channel);
if (channel.equals(channels.getDefault())) {
Msg.send(player, ChatFormatter.parse("&#9878C9Вы в канале [&#7858E9"
+ channel.display() + "&8 " + channel.prefix() + "&#9878C9] (дефолтный)"));
} else if (channel.equals(active)) {
Msg.send(player, ChatFormatter.parse("&#B798A8Вы уже в канале [&#7858E9"
+ channel.display() + "&8 " + channel.prefix() + "&#B798A8]"));
} else {
Msg.send(player, ChatFormatter.parse("&#9878C9Вы вошли в канал [&#7858E9"
+ channel.display() + "&8 " + channel.prefix() + "&#9878C9]"));
}
return true;
}
@Override
protected @Nullable List<String> getTabCompletions(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
if (args.length != 1) {
return null;
}
ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class);
String input = args[0].toLowerCase(Locale.ROOT);
return channels.getChannels().stream()
.filter(c -> !(sender instanceof Player p) || channels.canUse(p, c))
.map(Channel::id)
.filter(id -> id.toLowerCase(Locale.ROOT).startsWith(input))
.toList();
}
}

View file

@ -0,0 +1,39 @@
package com.loki.lochat.commands.chat;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChannelService;
import com.loki.lochat.commands.base.PlayerCommand;
import com.loki.lochat.utils.format.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* /leave — выйти из текущего канала (вернуться к дефолтному чату).
*/
public class LeaveChannelCommand extends PlayerCommand {
public LeaveChannelCommand(LoChat plugin) {
super(plugin);
}
@Override
protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class);
if (!channels.isEnabled()) {
Msg.send(player, ChatFormatter.parse("&#CF6679Система каналов отключена"));
return true;
}
if (!channels.isInChannel(player)) {
Msg.send(player, ChatFormatter.parse("&#B798A8Вы не находитесь в канале"));
return true;
}
channels.leave(player);
Msg.send(player, ChatFormatter.parse("&#9878C9Вы вышли из канала"));
return true;
}
}

View file

@ -1,11 +1,11 @@
package com.loki.lochat.commands.chat;
import com.loki.lochat.compat.Msg;
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 +14,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;
@ -31,11 +30,11 @@ public class LocalChatCommand implements CommandExecutor {
}
if (args.length == 0) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/l <сообщение>")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/l <сообщение>")));
return true;
}
chatService.sendLocalMessage(player, MM.deserialize(String.join(" ", args)));
chatService.sendLocalMessage(player, String.join(" ", args));
return true;
}
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.utils.format.ChatFormatter;
@ -28,26 +30,26 @@ public class IgnoreCommand implements CommandExecutor {
return true;
}
if (args.length == 0) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/ignore <ник>")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/ignore <ник>")));
return true;
}
Player target = Bukkit.getPlayer(args[0]);
if (target == null) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound()));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound()));
return true;
}
if (target.equals(player)) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.self")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.self")));
return true;
}
if (messagingService.isIgnoring(player.getUniqueId(), target.getUniqueId())) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.already-ignored")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.already-ignored")));
return true;
}
messagingService.addIgnore(player.getUniqueId(), target.getUniqueId());
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.added", "{player}", target.getName())));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.added", "{player}", target.getName())));
return true;
}
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.utils.format.ChatFormatter;
@ -33,18 +35,18 @@ public class IgnoreListCommand implements CommandExecutor {
Set<UUID> ignored = messagingService.getIgnoredPlayers(player.getUniqueId());
if (ignored.isEmpty()) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-empty")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-empty")));
return true;
}
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-header")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-header")));
for (UUID uuid : ignored) {
String name = PlayerUtil.getPlayerName(uuid);
if (name != null) {
player.sendMessage(ChatFormatter.parse("&#9878C9◆ &f" + name));
Msg.send(player, ChatFormatter.parse("&#9878C9◆ &f" + name));
}
}
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-footer")
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-footer")
.replace("{count}", String.valueOf(ignored.size()))));
return true;

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.api.service.pm.PrivateMessageService;
@ -34,25 +36,25 @@ public class MsgCommand implements CommandExecutor {
return true;
}
if (args.length < 2) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/msg <ник> <сообщение>")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/msg <ник> <сообщение>")));
return true;
}
Player target = Bukkit.getPlayer(args[0]);
if (target == null) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline")));
return true;
}
if (target.equals(player)) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.self")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("pm.self")));
return true;
}
if (messagingService.isIgnoring(target.getUniqueId(), player.getUniqueId())) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored")));
return true;
}
if (messagingService.isIgnoring(player.getUniqueId(), target.getUniqueId())) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored")));
return true;
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.api.service.pm.PrivateMessageService;
@ -37,23 +39,23 @@ public class ReplyCommand implements CommandExecutor {
return true;
}
if (args.length == 0) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/reply <сообщение>")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/reply <сообщение>")));
return true;
}
Optional<UUID> lastUuid = messagingService.getLastConversation(player.getUniqueId());
if (lastUuid.isEmpty()) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.no-reply")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("pm.no-reply")));
return true;
}
Player target = Bukkit.getPlayer(lastUuid.get());
if (target == null) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline")));
return true;
}
if (messagingService.isIgnoring(target.getUniqueId(), player.getUniqueId())) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored")));
return true;
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.utils.format.ChatFormatter;
@ -28,22 +30,22 @@ public class UnignoreCommand implements CommandExecutor {
return true;
}
if (args.length == 0) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/unignore <ник>")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/unignore <ник>")));
return true;
}
Player target = Bukkit.getPlayer(args[0]);
if (target == null) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound()));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound()));
return true;
}
if (!messagingService.isIgnoring(player.getUniqueId(), target.getUniqueId())) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.not-ignored")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.not-ignored")));
return true;
}
messagingService.removeIgnore(player.getUniqueId(), target.getUniqueId());
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.removed", "{player}", target.getName())));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.removed", "{player}", target.getName())));
return true;
}
}

View file

@ -1,157 +0,0 @@
package com.loki.lochat.commands.moderation.ban;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.MessagesConfig;
import com.loki.lochat.data.model.BanRecord;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.player.PlayerUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* /lban — бан с красивым киком
*/
public class BanCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final PunishmentService punishmentService;
private final MuteService muteService;
public BanCommand(LoChat plugin) {
this.plugin = plugin;
this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class);
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
MessagesConfig msg = plugin.getConfigManager().getMessagesConfig();
if (!sender.hasPermission("lochat.ban")) {
sender.sendMessage(ChatFormatter.parse(msg.getNoPermission()));
return true;
}
if (args.length < 1) {
sender.sendMessage(ChatFormatter.parse(msg.getBanUsage()));
sender.sendMessage(ChatFormatter.parse(msg.getMuteTimeHelp()));
return true;
}
String targetName = args[0];
UUID targetUuid = PlayerUtil.findPlayerUUID(targetName);
if (targetUuid == null) {
sender.sendMessage(ChatFormatter.parse(msg.getPlayerNotFound()));
return true;
}
if (sender instanceof Player p && p.getUniqueId().equals(targetUuid)) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Нельзя забанить самого себя"));
return true;
}
Player onlineTarget = Bukkit.getPlayer(targetUuid);
if (onlineTarget != null && onlineTarget.hasPermission("lochat.bypass.ban") && !sender.hasPermission("lochat.ban.override")) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Нельзя забанить этого игрока"));
return true;
}
if (punishmentService.isBanned(targetUuid)) {
sender.sendMessage(ChatFormatter.parse(msg.getAlreadyBanned()));
return true;
}
DurationResult parsed = parseDuration(sender, args, msg);
if (!parsed.valid) {
return true;
}
long durationMs = parsed.durationMs;
String reason = parsed.reason;
String finalName = PlayerUtil.getPlayerName(targetUuid);
if (finalName == null) {
finalName = targetName;
}
punishmentService.ban(targetUuid, finalName, durationMs, reason, sender.getName());
String durationLabel = durationMs == 0
? msg.getModerationDurationPermanent()
: muteService.formatTime(durationMs);
sender.sendMessage(ChatFormatter.parse(msg.getBanConfirm().replace("{player}", finalName).replace("{duration}", durationLabel)));
BanRecord ban = punishmentService.getActiveBan(targetUuid);
if (ban != null) {
Player kicked = Bukkit.getPlayer(targetUuid);
if (kicked != null && kicked.isOnline()) {
kicked.kick(punishmentService.buildBanKickMessage(ban));
}
}
return true;
}
private record DurationResult(long durationMs, String reason, boolean valid) { }
private DurationResult parseDuration(CommandSender sender, String[] args, MessagesConfig msg) {
String timeStr = null;
StringBuilder reasonBuilder = new StringBuilder();
for (int i = 1; i < args.length; i++) {
if (timeStr == null && isTimeFormat(args[i])) {
timeStr = args[i];
} else {
if (reasonBuilder.length() > 0) {
reasonBuilder.append(" ");
}
reasonBuilder.append(args[i]);
}
}
long durationMs;
if (timeStr == null || timeStr.isEmpty()) {
durationMs = 0;
} else if ("perm".equalsIgnoreCase(timeStr) || "0".equals(timeStr)) {
durationMs = 0;
} else {
durationMs = muteService.parseTime(timeStr);
if (durationMs < 0) {
sender.sendMessage(ChatFormatter.parse(msg.getInvalidTime()));
return new DurationResult(0, "", false);
}
}
String reason = reasonBuilder.length() > 0 ? reasonBuilder.toString() : msg.getModerationDefaultReason();
return new DurationResult(durationMs, reason, true);
}
private boolean isTimeFormat(String s) {
return "perm".equalsIgnoreCase(s) || s.matches("\\d+[dhms]?");
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> c = new ArrayList<>();
if (args.length == 1) {
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.getName().toLowerCase().startsWith(args[0].toLowerCase())) {
c.add(p.getName());
}
}
} else if (args.length == 2) {
c.addAll(List.of("1h", "1d", "7d", "30d", "perm"));
}
return c;
}
}

View file

@ -1,83 +0,0 @@
package com.loki.lochat.commands.moderation.ban;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.MessagesConfig;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.player.PlayerUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* /lunban — снять бан
*/
public class UnbanCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final PunishmentService punishmentService;
public UnbanCommand(LoChat plugin) {
this.plugin = plugin;
this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
MessagesConfig msg = plugin.getConfigManager().getMessagesConfig();
if (!sender.hasPermission("lochat.unban")) {
sender.sendMessage(ChatFormatter.parse(msg.getNoPermission()));
return true;
}
if (args.length < 1) {
sender.sendMessage(ChatFormatter.parse("&#B798A8Использование: &#7858E9/lunban <ник>"));
return true;
}
UUID uuid = PlayerUtil.findPlayerUUID(args[0]);
if (uuid == null) {
sender.sendMessage(ChatFormatter.parse(msg.getPlayerNotFound()));
return true;
}
if (!punishmentService.isBanned(uuid)) {
sender.sendMessage(ChatFormatter.parse(msg.getNotBanned()));
return true;
}
String name = PlayerUtil.getPlayerName(uuid);
if (name == null) {
name = args[0];
}
punishmentService.unban(uuid);
sender.sendMessage(ChatFormatter.parse(msg.getUnbanConfirm().replace("{player}", name)));
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> out = new ArrayList<>();
if (args.length == 1) {
String p = args[0].toLowerCase();
for (Player pl : Bukkit.getOnlinePlayers()) {
if (pl.getName().toLowerCase().startsWith(p)) {
out.add(pl.getName());
}
}
}
return out;
}
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.moderation.mute;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.data.model.MuteData;
@ -32,7 +34,7 @@ public class MuteBlameCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.muteblame")) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission"));
Msg.send(sender, plugin.getMessageConfig().getComponent("errors.no-permission"));
return true;
}
if (args.length < 1) {

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.moderation.mute;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.config.MessagesConfig;
@ -40,14 +42,14 @@ public class MuteCommand implements CommandExecutor, TabCompleter {
MessagesConfig hm = plugin.getConfigManager().getMessagesConfig();
if (!sender.hasPermission("lochat.mute")) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission"));
Msg.send(sender, plugin.getMessageConfig().getComponent("errors.no-permission"));
return true;
}
if (args.length < 1) {
sender.sendMessage(ChatFormatter.parse(hm.getMuteUsage()));
sender.sendMessage(ChatFormatter.parse(hm.getMuteTimeHelp()));
sender.sendMessage(ChatFormatter.parse(hm.getMuteSilentHelp()));
Msg.send(sender, ChatFormatter.parse(hm.getMuteUsage()));
Msg.send(sender, ChatFormatter.parse(hm.getMuteTimeHelp()));
Msg.send(sender, ChatFormatter.parse(hm.getMuteSilentHelp()));
return true;
}
@ -63,18 +65,18 @@ public class MuteCommand implements CommandExecutor, TabCompleter {
if (sender instanceof Player p && !muteService.canMuteForDuration(p, duration)) {
long max = muteService.getMaxDuration(p);
sender.sendMessage(ChatFormatter.parse("&#CF6679Нельзя мутить на такой срок! &#B798A8Максимум: &#7858E9" +
Msg.send(sender, ChatFormatter.parse("&#CF6679Нельзя мутить на такой срок! &#B798A8Максимум: &#7858E9" +
(max == 0 ? "навсегда" : muteService.formatTime(max))));
return true;
}
UUID targetUUID = com.loki.lochat.utils.player.PlayerUtil.findPlayerUUID(targetName);
if (targetUUID == null) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.player-not-found"));
Msg.send(sender, plugin.getMessageConfig().getComponent("errors.player-not-found"));
return true;
}
if (muteService.isMuted(targetUUID)) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Игрок уже замучен!"));
Msg.send(sender, ChatFormatter.parse("&#CF6679Игрок уже замучен!"));
return true;
}
@ -104,7 +106,7 @@ public class MuteCommand implements CommandExecutor, TabCompleter {
}
if (silent && !sender.hasPermission("lochat.mute.silent")) {
sender.sendMessage(ChatFormatter.parse(hm.getNoSilentMutePermission()));
Msg.send(sender, ChatFormatter.parse(hm.getNoSilentMutePermission()));
return null;
}
@ -131,7 +133,7 @@ public class MuteCommand implements CommandExecutor, TabCompleter {
}
long dur = muteService.parseTime(timeStr);
if (dur < 0) {
sender.sendMessage(ChatFormatter.parse(hm.getMuteTimeHelp()));
Msg.send(sender, ChatFormatter.parse(hm.getMuteTimeHelp()));
}
return dur;
}
@ -145,27 +147,27 @@ public class MuteCommand implements CommandExecutor, TabCompleter {
muteService.mute(targetUUID, finalName, duration, reason, op);
String timeDisplay = duration == 0 ? "навсегда" : muteService.formatTime(duration);
sender.sendMessage(ChatFormatter.parse("&#9878C9Игрок &#7858E9" + finalName + " &#9878C9замучен на &#7858E9" + timeDisplay));
sender.sendMessage(ChatFormatter.parse("&#B798A8Причина: &f" + reason));
Msg.send(sender, ChatFormatter.parse("&#9878C9Игрок &#7858E9" + finalName + " &#9878C9замучен на &#7858E9" + timeDisplay));
Msg.send(sender, ChatFormatter.parse("&#B798A8Причина: &f" + reason));
if (target != null) {
String youMsg = duration == 0
? hm.getMutedPermanent().replace("{reason}", reason)
: hm.getMutedMessage().replace("{reason}", reason).replace("{time}", timeDisplay);
target.sendMessage(ChatFormatter.parse(youMsg));
Msg.send(target, ChatFormatter.parse(youMsg));
}
if (silent) {
String msgText = hm.getPlayerMutedSilent().replace("{player}", finalName).replace("{time}", timeDisplay);
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.hasPermission("lochat.mute.see-silent")) {
p.sendMessage(ChatFormatter.parse(msgText));
Msg.send(p, ChatFormatter.parse(msgText));
}
}
} else {
String broadcastMsg = (duration == 0 ? hm.getPlayerMutedPermanent() : hm.getPlayerMuted())
.replace("{player}", finalName).replace("{time}", timeDisplay).replace("{reason}", reason);
Bukkit.broadcast(ChatFormatter.parse(broadcastMsg));
Msg.broadcast(ChatFormatter.parse(broadcastMsg));
}
String voiceCmd = plugin.getConfig().getString("moderation.voice-mute-console-command", "");

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.moderation.mute;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.data.model.MuteData;
@ -33,7 +35,7 @@ public class MuteHistoryCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.mutehistory")) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission"));
Msg.send(sender, plugin.getMessageConfig().getComponent("errors.no-permission"));
return true;
}
if (args.length < 1) {
@ -46,7 +48,7 @@ public class MuteHistoryCommand implements CommandExecutor, TabCompleter {
uuid = muteService.getUUIDByName(args[0]);
}
if (uuid == null) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.player-not-found"));
Msg.send(sender, plugin.getMessageConfig().getComponent("errors.player-not-found"));
return true;
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.moderation.mute;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.data.model.MuteData;
@ -29,7 +31,7 @@ public class MuteListCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.mutelist")) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission"));
Msg.send(sender, plugin.getMessageConfig().getComponent("errors.no-permission"));
return true;
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.moderation.mute;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.utils.format.ChatFormatter;
@ -30,11 +32,11 @@ public class UnmuteCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.unmute")) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission"));
Msg.send(sender, plugin.getMessageConfig().getComponent("errors.no-permission"));
return true;
}
if (args.length < 1) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Использование: /unmute <ник> [-s]"));
Msg.send(sender, ChatFormatter.parse("&#CF6679Использование: /unmute <ник> [-s]"));
return true;
}
@ -48,13 +50,13 @@ public class UnmuteCommand implements CommandExecutor, TabCompleter {
}
}
if (silent && !sender.hasPermission("lochat.mute.silent")) {
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoSilentUnmutePermission()));
Msg.send(sender, ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoSilentUnmutePermission()));
return true;
}
UUID uuid = com.loki.lochat.utils.player.PlayerUtil.findPlayerUUID(targetName);
if (uuid == null) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.player-not-found"));
Msg.send(sender, plugin.getMessageConfig().getComponent("errors.player-not-found"));
return true;
}
@ -63,22 +65,22 @@ public class UnmuteCommand implements CommandExecutor, TabCompleter {
String op = sender.getName();
if (muteService.unmute(uuid, op)) {
sender.sendMessage(ChatFormatter.parse("&#9878C9Игрок &#7858E9" + name + " &#9878C9размучен"));
Msg.send(sender, ChatFormatter.parse("&#9878C9Игрок &#7858E9" + name + " &#9878C9размучен"));
if (target != null) {
target.sendMessage(ChatFormatter.parse("&#9878C9Вы были &#7858E9размучены!"));
Msg.send(target, ChatFormatter.parse("&#9878C9Вы были &#7858E9размучены!"));
}
if (silent) {
String msg = "&#B798A8[&#9878C9ТИХО&#B798A8] &#7858E9" + name + " &#B798A8размучен &#9878C9(" + op + ")";
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.hasPermission("lochat.mute.see-silent")) {
p.sendMessage(ChatFormatter.parse(msg));
Msg.send(p, ChatFormatter.parse(msg));
}
}
} else {
Bukkit.broadcast(ChatFormatter.parse("&#9878C9Игрок &#7858E9" + name + " &#9878C9размучен модератором &#7858E9" + op));
Msg.broadcast(ChatFormatter.parse("&#9878C9Игрок &#7858E9" + name + " &#9878C9размучен модератором &#7858E9" + op));
}
} else {
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getPlayerNotMutedSimple()));
Msg.send(sender, ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getPlayerNotMutedSimple()));
}
return true;
}

View file

@ -1,90 +0,0 @@
package com.loki.lochat.commands.moderation.warn;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.MessagesConfig;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.player.PlayerUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* /silentwarn — варн в базе, цель не видит; модераторы с lochat.warn.notify видят
*/
public class SilentWarnCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final PunishmentService punishmentService;
public SilentWarnCommand(LoChat plugin) {
this.plugin = plugin;
this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
MessagesConfig msg = plugin.getConfigManager().getMessagesConfig();
if (!sender.hasPermission("lochat.warn.silent")) {
sender.sendMessage(ChatFormatter.parse(msg.getNoPermission()));
return true;
}
if (args.length < 1) {
sender.sendMessage(ChatFormatter.parse(msg.getSilentWarnUsage()));
return true;
}
UUID targetUuid = PlayerUtil.findPlayerUUID(args[0]);
if (targetUuid == null) {
sender.sendMessage(ChatFormatter.parse(msg.getPlayerNotFound()));
return true;
}
String mod = sender.getName();
String reason = args.length > 1
? String.join(" ", java.util.Arrays.copyOfRange(args, 1, args.length))
: msg.getModerationDefaultReason();
String name = PlayerUtil.getPlayerName(targetUuid);
if (name == null) {
name = args[0];
}
punishmentService.addWarn(targetUuid, name, mod, reason, true);
sender.sendMessage(ChatFormatter.parse(msg.getWarnStaffConfirm().replace("{player}", name).replace("{reason}", reason)));
String staffLine = msg.getSilentWarnStaff().replace("{player}", name).replace("{reason}", reason);
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.hasPermission("lochat.warn.notify")) {
p.sendMessage(ChatFormatter.parse(staffLine));
}
}
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> out = new ArrayList<>();
if (args.length == 1) {
String p = args[0].toLowerCase();
for (Player pl : Bukkit.getOnlinePlayers()) {
if (pl.getName().toLowerCase().startsWith(p)) {
out.add(pl.getName());
}
}
}
return out;
}
}

View file

@ -1,110 +0,0 @@
package com.loki.lochat.commands.moderation.warn;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.MessagesConfig;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.player.PlayerUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* /warn — игрок видит предупреждение в чате
*/
public class WarnCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final PunishmentService punishmentService;
public WarnCommand(LoChat plugin) {
this.plugin = plugin;
this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
MessagesConfig msg = plugin.getConfigManager().getMessagesConfig();
if (!sender.hasPermission("lochat.warn")) {
sender.sendMessage(ChatFormatter.parse(msg.getNoPermission()));
return true;
}
if (args.length < 1) {
sender.sendMessage(ChatFormatter.parse(msg.getWarnUsage()));
return true;
}
UUID targetUuid = PlayerUtil.findPlayerUUID(args[0]);
if (targetUuid == null) {
sender.sendMessage(ChatFormatter.parse(msg.getPlayerNotFound()));
return true;
}
if (sender instanceof Player p && p.getUniqueId().equals(targetUuid)) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Нельзя выдать варн самому себе"));
return true;
}
String mod = sender.getName();
String reason = buildReason(msg, args, 1);
String name = PlayerUtil.getPlayerName(targetUuid);
if (name == null) {
name = args[0];
}
Player targetOnline = Bukkit.getPlayer(targetUuid);
if (targetOnline != null && targetOnline.hasPermission("lochat.bypass.warn")) {
sender.sendMessage(ChatFormatter.parse("&#CF6679У этого игрока защита от варнов"));
return true;
}
punishmentService.addWarn(targetUuid, name, mod, reason, false);
sender.sendMessage(ChatFormatter.parse(msg.getWarnStaffConfirm().replace("{player}", name).replace("{reason}", reason)));
if (targetOnline != null && targetOnline.isOnline()) {
targetOnline.sendMessage(ChatFormatter.parse(msg.getWarnReceived().replace("{reason}", reason)));
}
return true;
}
private static String buildReason(MessagesConfig msg, String[] args, int from) {
if (args.length <= from) {
return msg.getModerationDefaultReason();
}
StringBuilder sb = new StringBuilder();
for (int i = from; i < args.length; i++) {
if (sb.length() > 0) {
sb.append(" ");
}
sb.append(args[i]);
}
return sb.toString();
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> out = new ArrayList<>();
if (args.length == 1) {
String p = args[0].toLowerCase();
for (Player pl : Bukkit.getOnlinePlayers()) {
if (pl.getName().toLowerCase().startsWith(p)) {
out.add(pl.getName());
}
}
}
return out;
}
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.nick;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.utils.format.ChatFormatter;
@ -29,55 +31,55 @@ public class NickCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text(plugin.getConfigManager().getMessagesConfig().getPlayerOnly(), NamedTextColor.RED));
Msg.send(sender, Component.text(plugin.getConfigManager().getMessagesConfig().getPlayerOnly(), NamedTextColor.RED));
return true;
}
if (!player.hasPermission("lochat.nick")) {
player.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission()));
Msg.send(player, ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission()));
return true;
}
if (args.length == 0) {
var nick = nickService.getNickname(player.getUniqueId());
if (nick.isPresent()) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.current", "{nick}", nick.get())));
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.reset-usage")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.current", "{nick}", nick.get())));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.reset-usage")));
} else {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.no-nick")));
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.set-usage")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.no-nick")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.set-usage")));
}
return true;
}
if (args[0].equalsIgnoreCase("reset")) {
if (nickService.getNickname(player.getUniqueId()).isEmpty()) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.no-nick")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.no-nick")));
return true;
}
nickService.resetNickname(player.getUniqueId());
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.reset-success")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.reset-success")));
return true;
}
String nickname = String.join(" ", args);
if (!nickService.isValidNickname(nickname)) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.invalid")));
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.requirements-header")));
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.requirements-length")));
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.requirements-chars")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.invalid")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.requirements-header")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.requirements-length")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.requirements-chars")));
return true;
}
if (nickService.isNicknameTaken(nickname)) {
var cur = nickService.getNickname(player.getUniqueId());
if (cur.isEmpty() || !cur.get().equals(nickname)) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.taken")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.taken")));
return true;
}
}
if (nickService.setNickname(player.getUniqueId(), nickname)) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.set-success", "{nick}", nickname)));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.set-success", "{nick}", nickname)));
} else {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.error")));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.error")));
}
return true;
}

View file

@ -1,11 +1,10 @@
package com.loki.lochat.commands.nick;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.data.model.BanRecord;
import com.loki.lochat.data.model.MuteData;
import com.loki.lochat.data.model.WarnEntry;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.player.PlayerUtil;
@ -23,35 +22,33 @@ import java.util.List;
import java.util.UUID;
/**
* /playerinfo — статистика варнов, банов и мутов
* /playerinfo — статистика мутов (mute-only)
*/
public class PlayerInfoCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final MuteService muteService;
private final PunishmentService punishmentService;
public PlayerInfoCommand(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.playerinfo")) {
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission()));
Msg.send(sender, ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission()));
return true;
}
if (args.length < 1) {
sender.sendMessage(ChatFormatter.parse("&#B798A8Использование: &#7858E9/playerinfo <ник>"));
Msg.send(sender, ChatFormatter.parse("&#B798A8Использование: &#7858E9/playerinfo <ник>"));
return true;
}
UUID uuid = PlayerUtil.findPlayerUUID(args[0]);
if (uuid == null) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Игрок не найден"));
Msg.send(sender, ChatFormatter.parse("&#CF6679Игрок не найден"));
return true;
}
@ -62,70 +59,40 @@ public class PlayerInfoCommand implements CommandExecutor, TabCompleter {
Player target = Bukkit.getPlayer(uuid);
sender.sendMessage(ChatFormatter.parse("<gradient:#7858E9:#B798A8><bold>═══════ Профиль: " + name + " ═══════</bold></gradient>"));
Msg.send(sender, ChatFormatter.parse("<gradient:#7858E9:#B798A8><bold>═══════ Профиль: " + name + " ═══════</bold></gradient>"));
if (target != null) {
sender.sendMessage(ChatFormatter.parse("&#9878C9❤ &fЗдоровье: &#7858E9" + Math.round(target.getHealth()) + "/20"));
sender.sendMessage(ChatFormatter.parse("&#9878C9✦ &fРежим: &#7858E9" + target.getGameMode().name().toLowerCase()));
sender.sendMessage(ChatFormatter.parse("&#9878C9◆ &fМир: &#7858E9" + target.getWorld().getName()));
Msg.send(sender, ChatFormatter.parse("&#9878C9❤ &fЗдоровье: &#7858E9" + Math.round(target.getHealth()) + "/20"));
Msg.send(sender, ChatFormatter.parse("&#9878C9✦ &fРежим: &#7858E9" + target.getGameMode().name().toLowerCase()));
Msg.send(sender, ChatFormatter.parse("&#9878C9◆ &fМир: &#7858E9" + target.getWorld().getName()));
} else {
sender.sendMessage(ChatFormatter.parse("&#B798A8Игрок &#7858E9не в сети"));
Msg.send(sender, ChatFormatter.parse("&#B798A8Игрок &#7858E9не в сети"));
}
if (plugin.getGradientModule() != null && target != null) {
sender.sendMessage(ChatFormatter.parse("&#9878C9⚡ &fГрадиент: " + plugin.getGradientModule().getFormattedName(target)));
}
int warns = punishmentService.getWarnCount(uuid);
sender.sendMessage(ChatFormatter.parse("&#CF6679⚠ &fВарнов всего: &#7858E9" + warns));
List<WarnEntry> recent = punishmentService.getWarns(uuid);
int show = Math.min(5, recent.size());
for (int i = recent.size() - show; i < recent.size(); i++) {
WarnEntry w = recent.get(i);
String tag = w.silent ? "&#B798A8[тихий] " : "";
String reason = w.reason != null ? w.reason : "—";
long t = w.time;
sender.sendMessage(ChatFormatter.parse(
tag + "&#9878C9• &7" + formatTime(t) + " &#B798A8от &#7858E9" + w.moderator + "&#9878C9: &f" + reason));
}
BanRecord ban = punishmentService.getActiveBan(uuid);
if (ban != null) {
String until = ban.isPermanent()
? "&#CF6679навсегда"
: "&#7858E9" + muteService.formatTime(Math.max(0, ban.until - System.currentTimeMillis()));
sender.sendMessage(ChatFormatter.parse("&#CF6679🚫 &fБан: &cактивен &7(" + until + "&7)"));
sender.sendMessage(ChatFormatter.parse("&#B798A8Причина: &f" + (ban.reason != null ? ban.reason : "—")));
} else {
sender.sendMessage(ChatFormatter.parse("&#9878C9🚫 &fБан: &aнет"));
Msg.send(sender, ChatFormatter.parse("&#9878C9⚡ &fГрадиент: " + plugin.getGradientModule().getFormattedName(target)));
}
if (muteService.isMuted(uuid)) {
MuteData md = muteService.getMuteData(uuid);
long rem = muteService.getRemainingTime(uuid);
String remStr = rem < 0 ? "навсегда" : muteService.formatTime(rem);
sender.sendMessage(ChatFormatter.parse("&#CF6679🔇 &fМут: &cда &7(" + remStr + "&7)"));
Msg.send(sender, ChatFormatter.parse("&#CF6679🔇 &fМут: &cда &7(" + remStr + "&7)"));
if (md != null) {
sender.sendMessage(ChatFormatter.parse("&#B798A8Причина: &f" + (md.getReason() != null ? md.getReason() : "—")));
Msg.send(sender, ChatFormatter.parse("&#B798A8Причина: &f" + (md.getReason() != null ? md.getReason() : "—")));
}
} else {
sender.sendMessage(ChatFormatter.parse("&#9878C9🔇 &fМут: &aнет"));
Msg.send(sender, ChatFormatter.parse("&#9878C9🔇 &fМут: &aнет"));
}
var muteHist = muteService.getPlayerHistory(uuid);
if (muteHist != null && !muteHist.isEmpty()) {
sender.sendMessage(ChatFormatter.parse("&#B798A8Записей в истории мутов: &#7858E9" + muteHist.size()));
Msg.send(sender, ChatFormatter.parse("&#B798A8Записей в истории мутов: &#7858E9" + muteHist.size()));
}
return true;
}
private static String formatTime(long ms) {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm");
return sdf.format(new java.util.Date(ms));
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.commands.base.PlayerCommand;
@ -37,7 +39,7 @@ public class DoCommand extends PlayerCommand {
int remaining = playerService.getRemainingCooldown(player.getUniqueId(), "rp_do", cooldown);
String msg = plugin.getConfigManager().getMessagesConfig().getCooldownMessage()
.replace("{remaining}", String.valueOf(remaining));
player.sendMessage(ChatFormatter.parse(msg));
Msg.send(player, ChatFormatter.parse(msg));
return true;
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.commands.base.PlayerCommand;
@ -37,7 +39,7 @@ public class MeCommand extends PlayerCommand {
int remaining = playerService.getRemainingCooldown(player.getUniqueId(), "rp_me", cooldown);
String msg = plugin.getConfigManager().getMessagesConfig().getCooldownMessage()
.replace("{remaining}", String.valueOf(remaining));
player.sendMessage(ChatFormatter.parse(msg));
Msg.send(player, ChatFormatter.parse(msg));
return true;
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.commands.base.PlayerCommand;
import com.loki.lochat.config.RatConfig;
@ -41,19 +43,19 @@ public class RollCommand extends PlayerCommand {
try {
max = Integer.parseInt(args[0]);
if (max < 2) {
player.sendMessage(ChatFormatter.parse(
Msg.send(player, ChatFormatter.parse(
plugin.getConfigManager().getString("rp.roll-min-error",
"<red>Минимальное значение кубика: 2</red>")));
return true;
}
if (max > RatConfig.ROLL_ABSOLUTE_MAX) {
player.sendMessage(ChatFormatter.parse(
Msg.send(player, ChatFormatter.parse(
plugin.getConfigManager().getString("rp.roll-max-error",
"<red>Максимальное значение кубика: 1000000</red>")));
return true;
}
} catch (NumberFormatException e) {
player.sendMessage(ChatFormatter.parse(
Msg.send(player, ChatFormatter.parse(
plugin.getConfigManager().getString("rp.roll-invalid-number",
"<red>Укажите число: /roll [максимум]</red>")));
return true;

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.player.DistanceUtil;
@ -22,7 +24,7 @@ public final class RpUtil {
public static void sendToRadius(Player sender, Component message, int radius) {
for (Player player : Bukkit.getOnlinePlayers()) {
if (DistanceUtil.isInRange(sender, player, radius)) {
player.sendMessage(message);
Msg.send(player, message);
}
}
}
@ -39,8 +41,10 @@ public final class RpUtil {
} catch (Exception ignored) {
}
// Пробуем взять displayName (может содержать градиент от NickService)
Component displayName = player.displayName();
// Пробуем взять displayName (может содержать градиент от NickService) — Spigot: legacy String
String displayNameLegacy = player.getDisplayName();
Component displayName = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer
.legacySection().deserialize(displayNameLegacy);
String plain = PlainTextComponentSerializer.plainText().serialize(displayName);
// Если displayName == обычное имя — возвращаем его напрямую (без потери цветов не нужно)
if (plain.equals(player.getName())) {

View file

@ -1,5 +1,7 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.commands.base.PlayerCommand;
@ -40,7 +42,7 @@ public class TryCommand extends PlayerCommand {
int remaining = playerService.getRemainingCooldown(player.getUniqueId(), "rp_try", cooldown);
String msg = plugin.getConfigManager().getMessagesConfig().getCooldownMessage()
.replace("{remaining}", String.valueOf(remaining));
player.sendMessage(ChatFormatter.parse(msg));
Msg.send(player, ChatFormatter.parse(msg));
return true;
}

View file

@ -0,0 +1,50 @@
package com.loki.lochat.compat;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.Nullable;
/**
* Holds BukkitAudiences for Spigot. Falls back to legacy text if creation fails.
*/
public final class Adventure {
private static BukkitAudiences audiences;
private static boolean fallback;
private Adventure() {
}
public static void init(Plugin plugin) {
try {
audiences = BukkitAudiences.create(plugin);
fallback = false;
} catch (Throwable t) {
audiences = null;
fallback = true;
plugin.getLogger().warning(
"[LoChat] BukkitAudiences init failed, falling back to legacy text: " + t.getMessage());
}
}
public static void close() {
if (audiences != null) {
try {
audiences.close();
} catch (Exception ignored) {
}
audiences = null;
}
fallback = false;
}
@Nullable
public static BukkitAudiences audiences() {
return audiences;
}
public static boolean isFallback() {
return fallback || audiences == null;
}
}

View file

@ -0,0 +1,174 @@
package com.loki.lochat.compat;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.ItemMeta;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
/**
* Static helpers for sending Adventure components on Spigot via BukkitAudiences.
* Falls back to legacy String when audiences not available.
*/
public final class Msg {
private static final LegacyComponentSerializer LEGACY = LegacyComponentSerializer.builder()
.character('§')
.hexColors()
.useUnusualXRepeatedCharacterHexFormat()
.build();
private static final PlainTextComponentSerializer PLAIN = PlainTextComponentSerializer.plainText();
private Msg() {
}
public static void send(CommandSender sender, Component component) {
if (sender == null || component == null) {
return;
}
if (!Adventure.isFallback() && Adventure.audiences() != null) {
try {
Adventure.audiences().sender(sender).sendMessage(component);
return;
} catch (Throwable ignored) {
}
}
sender.sendMessage(legacy(component));
}
public static void send(Collection<? extends CommandSender> senders, Component component) {
if (senders == null || component == null) {
return;
}
for (CommandSender s : senders) {
send(s, component);
}
}
public static void broadcast(Component component) {
if (component == null) {
return;
}
if (!Adventure.isFallback() && Adventure.audiences() != null) {
try {
Adventure.audiences().all().sendMessage(component);
return;
} catch (Throwable ignored) {
}
}
String legacy = legacy(component);
Bukkit.getOnlinePlayers().forEach(p -> p.sendMessage(legacy));
Bukkit.getConsoleSender().sendMessage(legacy);
}
public static void actionBar(Player player, Component component) {
if (player == null || component == null) {
return;
}
if (!Adventure.isFallback() && Adventure.audiences() != null) {
try {
Adventure.audiences().player(player).sendActionBar(component);
return;
} catch (Throwable ignored) {
}
}
// Fallback: send as chat
player.sendMessage(legacy(component));
}
public static void title(Player player, Component title, Component subtitle, Title.Times times) {
if (player == null) {
return;
}
Component t = title != null ? title : Component.empty();
Component s = subtitle != null ? subtitle : Component.empty();
Title.Times tm = times != null ? times : Title.Times.times(
Duration.ofMillis(500), Duration.ofMillis(3000), Duration.ofMillis(500));
if (!Adventure.isFallback() && Adventure.audiences() != null) {
try {
Adventure.audiences().player(player).showTitle(Title.title(t, s, tm));
return;
} catch (Throwable ignored) {
}
}
// Fallback via Bukkit API (legacy strings, 1.20.1 supports titles)
player.sendTitle(legacy(t), legacy(s),
(int) (tm.fadeIn().toMillis() / 50),
(int) (tm.stay().toMillis() / 50),
(int) (tm.fadeOut().toMillis() / 50));
}
public static String legacy(Component component) {
if (component == null) {
return "";
}
return LEGACY.serialize(component);
}
public static String plain(Component component) {
if (component == null) {
return "";
}
return PLAIN.serialize(component);
}
public static void setDisplayName(Player player, Component component) {
if (player == null) {
return;
}
player.setDisplayName(component == null ? "" : legacy(component));
}
public static void setPlayerListName(Player player, Component component) {
if (player == null) {
return;
}
player.setPlayerListName(component == null ? "" : legacy(component));
}
public static void kick(Player player, Component reason) {
if (player == null) {
return;
}
String msg = reason == null ? "" : legacy(reason);
// Must be on main thread; caller should ensure via scheduler if needed
if (Bukkit.isPrimaryThread()) {
player.kickPlayer(msg);
} else {
Bukkit.getScheduler().runTask(
Bukkit.getPluginManager().getPlugin("LoChat"), () -> player.kickPlayer(msg));
}
}
public static void itemName(ItemMeta meta, Component name) {
if (meta == null) {
return;
}
meta.setDisplayName(name == null ? null : legacy(name));
}
public static void itemLore(ItemMeta meta, List<Component> lore) {
if (meta == null) {
return;
}
if (lore == null) {
meta.setLore(null);
return;
}
List<String> lines = lore.stream().map(Msg::legacy).toList();
meta.setLore(lines);
}
public static String inventoryTitle(Component title) {
return legacy(title);
}
}

View file

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

View file

@ -68,14 +68,14 @@ public abstract class BaseConfig {
if (!configFile.exists()) {
try {
String resourcePath = inConfigFolder ? "config/" + fileName : fileName;
InputStream in = plugin.getResource(resourcePath);
try (InputStream in = plugin.getResource(resourcePath)) {
if (in != null) {
Files.copy(in, configFile.toPath());
in.close();
plugin.getLogger().info("Created config file: " + (inConfigFolder ? "config/" : "") + fileName);
} else {
plugin.getLogger().warning("Resource not found: " + resourcePath);
}
}
} catch (Exception e) {
plugin.getLogger().warning("Failed to copy " + fileName + ": " + e.getMessage());
}

View file

@ -0,0 +1,99 @@
package com.loki.lochat.config;
import com.loki.lochat.channel.Channel;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Конфигурация каналов чата (config/channels.yml).
*/
public class ChannelConfig extends BaseConfig {
private volatile Map<String, Channel> channels = new LinkedHashMap<>();
private volatile Map<String, Channel> byAlias = new LinkedHashMap<>();
private volatile Channel defaultChannel;
public ChannelConfig(JavaPlugin plugin) {
super(plugin, "channels.yml", true);
}
@Override
protected void onLoad() {
loadChannels();
buildAliasIndex();
}
private void loadChannels() {
Map<String, Channel> result = new LinkedHashMap<>();
ConfigurationSection section = getConfig().getConfigurationSection("channels.list");
if (section != null) {
for (String id : section.getKeys(false)) {
String path = "channels.list." + id;
result.put(id, new Channel(
id,
getConfig().getString(path + ".display", ""),
getConfig().getString(path + ".prefix", id.toUpperCase(Locale.ROOT)),
getConfig().getString(path + ".color", "&7"),
getConfig().getInt(path + ".radius", -1),
getConfig().getInt(path + ".cooldown", 0),
getConfig().getString(path + ".permission", ""),
getConfig().getStringList(path + ".aliases")));
}
}
this.channels = result;
}
private void buildAliasIndex() {
Map<String, Channel> index = new LinkedHashMap<>();
for (Channel channel : channels.values()) {
index.put(channel.id().toLowerCase(Locale.ROOT), channel);
for (String alias : channel.aliases()) {
if (alias == null || alias.isBlank()) {
continue;
}
index.putIfAbsent(alias.toLowerCase(Locale.ROOT), channel);
}
}
this.byAlias = index;
String defaultId = getConfig().getString("channels.default", "global");
this.defaultChannel = channels.get(defaultId);
if (defaultChannel == null) {
this.defaultChannel = channels.isEmpty() ? null : channels.values().iterator().next();
}
}
public boolean isEnabled() {
return getConfig().getBoolean("channels.enabled", true);
}
public Channel getDefault() {
return defaultChannel;
}
public Channel get(String id) {
return id == null ? null : channels.get(id.toLowerCase(Locale.ROOT));
}
/**
* Поиск канала по имени или алиасу (без учёта регистра).
*/
public Channel find(String name) {
return name == null ? null : byAlias.get(name.toLowerCase(Locale.ROOT));
}
public Collection<Channel> getChannels() {
return channels.values();
}
public List<Channel> getChannelList() {
return List.copyOf(channels.values());
}
}

View file

@ -21,6 +21,7 @@ public class ConfigManager {
private volatile MuteConfig muteConfig;
private volatile FiltersConfig filtersConfig;
private volatile SoundsConfig soundsConfig;
private volatile ChannelConfig channelConfig;
public ConfigManager(LoChat plugin) {
this.plugin = plugin;
@ -33,6 +34,7 @@ public class ConfigManager {
this.muteConfig = new MuteConfig(plugin);
this.filtersConfig = new FiltersConfig(plugin);
this.soundsConfig = new SoundsConfig(plugin);
this.channelConfig = new ChannelConfig(plugin);
// Инициализируем все конфиги
this.appearanceConfig.init();
@ -40,6 +42,7 @@ public class ConfigManager {
this.muteConfig.init();
this.filtersConfig.init();
this.soundsConfig.init();
this.channelConfig.init();
loader.updateConfig();
}
@ -66,6 +69,10 @@ public class ConfigManager {
return soundsConfig;
}
public ChannelConfig getChannelConfig() {
return channelConfig;
}
// ========== Обновление и перезагрузка ==========
public synchronized void reload() {
@ -77,6 +84,7 @@ public class ConfigManager {
this.muteConfig.reload();
this.filtersConfig.reload();
this.soundsConfig.reload();
this.channelConfig.reload();
plugin.getLogger().info("Все конфигурации успешно перезагружены");
}
@ -241,6 +249,26 @@ public class ConfigManager {
return accessor.getTranslationApiKey();
}
public String getTranslationProvider() {
return accessor.getTranslationProvider();
}
public String getTranslationMode() {
return accessor.getTranslationMode();
}
public int getTranslationTimeoutMs() {
return accessor.getTranslationTimeoutMs();
}
public java.util.List<String> getTranslationHoverFormat() {
return accessor.getTranslationHoverFormat();
}
public String getTranslationBypassPermission() {
return accessor.getTranslationBypassPermission();
}
// ========== LoPreff integration ==========
public boolean isLopreffEnabled() {

View file

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

View file

@ -1,5 +1,7 @@
package com.loki.lochat.config;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.format.ChatFormatter;
@ -64,14 +66,14 @@ public class MessageConfig {
* Отправляет сообщение игроку
*/
public void send(CommandSender sender, String path) {
sender.sendMessage(getComponent(path));
Msg.send(sender, getComponent(path));
}
/**
* Отправляет сообщение игроку с заменами
*/
public void send(CommandSender sender, String path, String... replacements) {
sender.sendMessage(getComponent(path, replacements));
Msg.send(sender, getComponent(path, replacements));
}
// Shortcuts

View file

@ -157,7 +157,7 @@ public class MessagesConfig extends BaseConfig {
return getConfig().getString("ignore.ignore_list_header", "&#B798A8Игнорируемые игроки&#9878C9:");
}
// ── Модерация (варны / баны) ────────────────────────────────────────────────
// ── Модерация ────────────────────────────────────────────────────────────────
public String getModerationDefaultReason() {
return getConfig().getString("moderation.default_reason", "Без причины");
@ -166,63 +166,4 @@ public class MessagesConfig extends BaseConfig {
public String getModerationDurationPermanent() {
return getConfig().getString("moderation.duration_permanent", "навсегда");
}
/**
* MiniMessage + legacy &# цвета; плейсхолдеры {reason},{moderator},{duration},{player}; перевод строки \\n
*/
public String getBanKickMessage() {
return getConfig().getString("moderation.ban_kick_message", defaultBanKick());
}
private String defaultBanKick() {
return "<gradient:#FF5555:#8B0000><bold>═══════ ДОСТУП ЗАПРЕЩЁН ═══════</bold></gradient>\n"
+ "<gray>Вы заблокированы на этом сервере.</gray>\n"
+ "<white>Причина: <gradient:#7858E9:#B798A8>{reason}</gradient></white>\n"
+ "<gray>Модератор: <white>{moderator}</white></gray>\n"
+ "<gray>Срок: <#7858E9>{duration}</#7858E9></gray>\n"
+ "<gradient:#FF5555:#8B0000><bold>════════════════════════</bold></gradient>";
}
public String getWarnReceived() {
return getConfig().getString("moderation.warn_received", "&#CF6679⚠ Вам выдано предупреждение! &#B798A8Причина: &#7858E9{reason}");
}
public String getWarnStaffConfirm() {
return getConfig().getString("moderation.warn_staff_confirm", "&#9878C9Игроку &#7858E9{player} &#9878C9выдан"
+ "варн. &#B798A8Причина: &#7858E9{reason}");
}
public String getSilentWarnStaff() {
return getConfig().getString("moderation.silent_warn_staff",
"&#B798A8[&#9878C9ТИХИЙ ВАРН&#B798A8] &#7858E9{player}&#9878C9: &#B798A8{reason}");
}
public String getBanUsage() {
return getConfig().getString("moderation.ban_usage", "&#B798A8Использование: &#7858E9/lban <ник> [время|perm] [причина]");
}
public String getBanConfirm() {
return getConfig().getString("moderation.ban_confirm", "&#9878C9Игрок &#7858E9{player} &#9878C9забанен."
+ "&#B798A8Срок: &#7858E9{duration}");
}
public String getUnbanConfirm() {
return getConfig().getString("moderation.unban_confirm", "&#9878C9Бан снят с &#7858E9{player}");
}
public String getNotBanned() {
return getConfig().getString("moderation.not_banned", "&#CF6679Игрок не забанен!");
}
public String getAlreadyBanned() {
return getConfig().getString("moderation.already_banned", "&#CF6679Игрок уже забанен!");
}
public String getWarnUsage() {
return getConfig().getString("moderation.warn_usage", "&#B798A8Использование: &#7858E9/warn <ник> [причина]");
}
public String getSilentWarnUsage() {
return getConfig().getString("moderation.silent_warn_usage", "&#B798A8Использование: &#7858E9/silentwarn <ник> [причина]");
}
}

View file

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

View file

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

View file

@ -150,6 +150,29 @@ public class ConfigAccessor {
return config.getString("chat.translation.api-key", "");
}
/** "libretranslate" (needs endpoint and usually a key) or "google" (free, no key). */
public String getTranslationProvider() {
return config.getString("chat.translation.provider", "libretranslate");
}
/** "hover" shows the translation when hovering the message, "actionbar" shows it above the hotbar. */
public String getTranslationMode() {
return config.getString("chat.translation.mode", "hover");
}
public int getTranslationTimeoutMs() {
return config.getInt("chat.translation.timeout-ms", 3000);
}
public java.util.List<String> getTranslationHoverFormat() {
java.util.List<String> lines = config.getStringList("chat.translation.hover-format");
return lines.isEmpty() ? java.util.List.of("&7Translation:", "&f{translation}") : lines;
}
public String getTranslationBypassPermission() {
return config.getString("chat.translation.bypass-permission", "lochat.translate.bypass");
}
// ========== LoPreff integration ==========
public boolean isLopreffEnabled() {

View file

@ -5,18 +5,15 @@ import com.loki.lochat.api.service.MessageService;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.ConfigManager;
import com.loki.lochat.config.MessageConfig;
import com.loki.lochat.core.filter.AdvancedMessageFilter;
import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.gradient.GradientModule;
import com.loki.lochat.integrations.DiscordIntegration;
import com.loki.lochat.integrations.LibertyBansHook;
import com.loki.lochat.integrations.PlaceholderAPIHook;
import com.loki.lochat.listener.ChatEventListener;
import com.loki.lochat.listener.DiscordEventListener;
import com.loki.lochat.listener.ModerationListener;
import com.loki.lochat.listener.PlayerEventListener;
import com.loki.lochat.managers.AutoMessageManager;
import com.loki.lochat.managers.CustomCommandManager;
@ -70,9 +67,6 @@ public class PluginInitializer {
* Инициализировать интеграции
*/
public void initIntegrations(DiscordIntegration discordIntegration, GradientModule gradientModule) {
// LibertyBans
new LibertyBansHook(plugin);
// PlaceholderAPI
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
new PlaceholderAPIHook(plugin).register();
@ -103,19 +97,17 @@ public class PluginInitializer {
*/
public AdvancedMessageFilter registerListeners(ServiceRegistry serviceRegistry, DiscordIntegration discordIntegration,
MessageService messageService, PlayerService playerService,
MessagingService messagingService, NickService nickService,
PunishmentService punishmentService) {
MessagingService messagingService, NickService nickService) {
AdvancedMessageFilter filter = new AdvancedMessageFilter(plugin.getConfig(), plugin);
plugin.setChatFilter(filter);
plugin.getServer().getPluginManager().registerEvents(
new ChatEventListener(plugin, messageService, playerService, filter), plugin
new ChatEventListener(plugin, messageService, playerService, filter,
serviceRegistry.get(com.loki.lochat.api.service.ChannelService.class)), plugin
);
plugin.getServer().getPluginManager().registerEvents(
new PlayerEventListener(plugin, playerService, messagingService, nickService, filter), plugin
);
plugin.getServer().getPluginManager().registerEvents(
new ModerationListener(punishmentService), plugin
);
// Discord listener
if (discordIntegration.isEnabled()) {

View file

@ -5,7 +5,6 @@ import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.gradient.GradientModule;
import com.loki.lochat.integrations.DiscordIntegration;
@ -82,11 +81,6 @@ public class PluginShutdown {
muteService.save();
}
PunishmentService punishmentService = serviceRegistry.get(PunishmentService.class);
if (punishmentService != null) {
punishmentService.save();
}
// Ники
NickService nickService = serviceRegistry.get(NickService.class);
if (nickService != null) {

View file

@ -1,5 +1,7 @@
package com.loki.lochat.core.filter;
import com.loki.lochat.LoChat;
import com.loki.lochat.config.FiltersConfig;
import com.loki.lochat.core.filter.filters.CharacterFilter;
import com.loki.lochat.core.filter.filters.FloodFilter;
import com.loki.lochat.core.filter.filters.SpamFilter;
@ -17,47 +19,41 @@ 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;
private final SpamFilter spamFilter;
private final UrlFilter urlFilter;
private final CharacterFilter characterFilter;
public AdvancedMessageFilter(FileConfiguration config) {
this(config, null);
}
private final FilterStatistics statistics = new FilterStatistics();
public AdvancedMessageFilter(FileConfiguration config, JavaPlugin plugin) {
this.config = config;
this.filtersConfig = resolveFiltersConfig(plugin);
// Инициализация CapsFilter
com.loki.lochat.config.FiltersConfig filtersConfig = null;
if (plugin instanceof com.loki.lochat.LoChat loChat) {
filtersConfig = 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);
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);
}
// Инициализация остальных фильтров
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);
private static FiltersConfig resolveFiltersConfig(JavaPlugin plugin) {
if (plugin instanceof LoChat loChat) {
return loChat.getConfigManager().getFiltersConfig();
}
if (plugin != null) {
FiltersConfig filtersConfig = new FiltersConfig(plugin);
filtersConfig.init();
return filtersConfig;
}
throw new IllegalArgumentException("AdvancedMessageFilter requires a FiltersConfig");
}
/**
@ -74,63 +70,70 @@ 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) {
statistics.increment("caps");
return FilterResult.blocked("&#CF6679Не кричите в чате");
}
message = filtered;
}
// 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()) {
statistics.increment("swear");
return swear;
}
message = swear.filteredMessage();
}
// 3. URL фильтры
if (config.getBoolean("filters.advertising.enabled", true)) {
if (filtersConfig.isAdvertisingEnabled()) {
FilterResult r = urlFilter.filterUrls(player, message);
if (!r.allowed()) {
statistics.increment("url");
return r;
}
message = r.filteredMessage();
r = urlFilter.filterHiddenUrls(player, message);
if (!r.allowed()) {
statistics.increment("hidden-url");
return r;
}
}
// 4. IP
if (config.getBoolean("filters.ip.enabled", true)) {
if (filtersConfig.isIpEnabled()) {
FilterResult r = urlFilter.filterIPs(player, message);
if (!r.allowed()) {
statistics.increment("ip");
return r;
}
message = r.filteredMessage();
}
// 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()) {
statistics.increment("flood");
return r;
}
}
// 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()) {
statistics.increment("spam");
return r;
}
}
@ -138,6 +141,13 @@ public class AdvancedMessageFilter {
return FilterResult.ok(message);
}
/**
* Статистика блокировок по фильтрам (с момента запуска).
*/
public FilterStatistics getStatistics() {
return statistics;
}
/**
* Очистка данных игрока при выходе
*/

View file

@ -0,0 +1,43 @@
package com.loki.lochat.core.filter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
/**
* Счётчики заблокированных сообщений по каждому фильтру (с момента запуска / сброса).
*/
public final class FilterStatistics {
private final Map<String, LongAdder> blocked = new ConcurrentHashMap<>();
public void increment(String filterType) {
blocked.computeIfAbsent(filterType, k -> new LongAdder()).increment();
}
public long getBlocked(String filterType) {
LongAdder counter = blocked.get(filterType);
return counter == null ? 0L : counter.sum();
}
public long getTotalBlocked() {
return blocked.values().stream().mapToLong(LongAdder::sum).sum();
}
/**
* Снимок счётчиков, отсортированный по убыванию (заблокировавших больше всего — первыми).
*/
public Map<String, Long> snapshot() {
return blocked.entrySet().stream()
.sorted(Map.Entry.<String, LongAdder>comparingByValue(
(a, b) -> Long.compare(b.sum(), a.sum())))
.collect(LinkedHashMap::new,
(m, e) -> m.put(e.getKey(), e.getValue().sum()),
LinkedHashMap::putAll);
}
public void reset() {
blocked.clear();
}
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.core.filter;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.api.filter.MessageFilter;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.config.MessagesConfig;
@ -49,6 +51,6 @@ public class MuteFilter implements MessageFilter {
.replace("{time}", timeStr);
}
player.sendMessage(ChatFormatter.parse(msg));
Msg.send(player, ChatFormatter.parse(msg));
}
}

View file

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

View file

@ -1,22 +1,25 @@
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;
import java.util.Deque;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
public class FloodFilter {
private final FileConfiguration config;
private final Map<UUID, Deque<Long>> floodTracker = new ConcurrentHashMap<>();
private static final String FLOOD_BLOCK_MESSAGE = "&#CF6679Не флудите!";
public FloodFilter(FileConfiguration config) {
this.config = config;
private final int maxMessages;
private final int timePeriod;
private final Map<UUID, ConcurrentLinkedDeque<Long>> floodTracker = new ConcurrentHashMap<>();
public FloodFilter(FiltersConfig filters) {
this.maxMessages = filters.getFloodMaxMessages();
this.timePeriod = filters.getFloodTimePeriod();
}
public FilterResult filter(Player player) {
@ -24,13 +27,11 @@ 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;
Deque<Long> timestamps = floodTracker.computeIfAbsent(
player.getUniqueId(), k -> new ArrayDeque<>());
ConcurrentLinkedDeque<Long> timestamps = floodTracker.computeIfAbsent(
player.getUniqueId(), k -> new ConcurrentLinkedDeque<>());
// Удаляем старые timestamp'ы
while (!timestamps.isEmpty() && now - timestamps.peekFirst() > windowMs) {
@ -38,8 +39,7 @@ public class FloodFilter {
}
if (timestamps.size() >= maxMessages) {
return FilterResult.blocked(config.getString("filters.flood.block-message",
"&#CF6679Не флудите!"));
return FilterResult.blocked(FLOOD_BLOCK_MESSAGE);
}
timestamps.addLast(now);

View file

@ -1,22 +1,30 @@
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;
import java.util.Deque;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
public class SpamFilter {
private final FileConfiguration config;
private final Map<UUID, Deque<String>> spamTracker = new ConcurrentHashMap<>();
private final int maxSimilar;
private final int threshold;
private final String blockMessage;
private final Map<UUID, ConcurrentLinkedDeque<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,11 +32,8 @@ 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<>());
ConcurrentLinkedDeque<String> history = spamTracker.computeIfAbsent(
player.getUniqueId(), k -> new ConcurrentLinkedDeque<>());
// Считаем похожие сообщения
long similarCount = history.stream()
@ -36,8 +41,7 @@ public class SpamFilter {
.count();
if (similarCount >= maxSimilar) {
return FilterResult.blocked(config.getString("filters.spam.block-message",
"&#CF6679Не отправляйте одинаковые сообщения"));
return FilterResult.blocked(blockMessage);
}
// Добавляем в историю (храним последние 10)

View file

@ -1,9 +1,10 @@
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 com.loki.lochat.utils.text.SwearNormalizer;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
@ -13,17 +14,16 @@ import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
public class SwearFilter {
private final boolean enabled;
private final boolean replaceMode;
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());
if (enabled) {
loadWordList(plugin);
@ -49,13 +49,30 @@ public class SwearFilter {
return FilterResult.ok(message);
}
if (ahoCorasick.matches(message)) {
String normalized = SwearNormalizer.normalizeForSearch(message);
if (ahoCorasick.matches(normalized)) {
if (replaceMode) {
return FilterResult.ok(ahoCorasick.replaceAll(message, replacement));
return FilterResult.ok(maskMatches(message, normalized));
}
return FilterResult.blocked("Сообщение содержит запрещенные слова");
}
return FilterResult.ok(message);
}
/**
* Маскирует в оригинальном сообщении каждое найденное (возможно, обфусцированное) слово.
*/
private String maskMatches(String original, String normalized) {
String result = original;
for (String word : ahoCorasick.findMatches(normalized)) {
Matcher matcher = SwearNormalizer.wordPattern(word).matcher(result);
result = matcher.replaceAll(maskFor(word.length()));
}
return result;
}
private static String maskFor(int wordLength) {
return "*".repeat(Math.max(1, wordLength));
}
}

View file

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

View file

@ -1,6 +1,7 @@
package com.loki.lochat.core.registry;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChannelService;
import com.loki.lochat.api.service.ChatService;
import com.loki.lochat.api.service.MentionService;
import com.loki.lochat.api.service.MessageService;
@ -8,17 +9,16 @@ import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.api.service.pm.PrivateMessageService;
import com.loki.lochat.config.ConfigManager;
import com.loki.lochat.config.MessageConfig;
import com.loki.lochat.core.factory.ServiceFactory;
import com.loki.lochat.core.service.ChatServiceImpl;
import com.loki.lochat.core.service.ChannelServiceImpl;
import com.loki.lochat.core.service.MentionServiceImpl;
import com.loki.lochat.core.service.MessageServiceImpl;
import com.loki.lochat.core.service.NickServiceImpl;
import com.loki.lochat.core.service.PlayerServiceImpl;
import com.loki.lochat.core.service.PunishmentServiceImpl;
import com.loki.lochat.core.service.messaging.PrivateMessageServiceImpl;
import org.bukkit.plugin.java.JavaPlugin;
@ -47,10 +47,14 @@ 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));
register(ChannelService.class, new ChannelServiceImpl(configManager.getChannelConfig()));
PrivateMessageServiceImpl pmService = new PrivateMessageServiceImpl((LoChat) plugin);
MessageService messageService = new MessageServiceImpl(plugin, muteService, playerService);
register(MessageService.class, messageService);
register(ChatService.class, new ChatServiceImpl(plugin, messageService));
PrivateMessageServiceImpl pmService = new PrivateMessageServiceImpl((LoChat) plugin, playerService);
MessagingService messagingService = ServiceFactory.createMessagingService(plugin, messageConfig, pmService);
pmService.init(messagingService);
register(PrivateMessageService.class, pmService);
@ -58,7 +62,6 @@ public class ServiceRegistry {
register(MentionService.class, new MentionServiceImpl(configManager));
register(NickService.class, new NickServiceImpl(plugin));
register(PunishmentService.class, new PunishmentServiceImpl(plugin, configManager.getMessagesConfig()));
}
public <T> void register(Class<T> serviceClass, T implementation) {

View file

@ -0,0 +1,85 @@
package com.loki.lochat.core.service;
import com.loki.lochat.api.service.ChannelService;
import com.loki.lochat.channel.Channel;
import com.loki.lochat.config.ChannelConfig;
import org.bukkit.entity.Player;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Реализация сервиса каналов чата.
* Хранит активный (присоединённый) канал каждого игрока в памяти.
*/
public class ChannelServiceImpl implements ChannelService {
private final ChannelConfig config;
private final Map<UUID, String> activeChannels = new ConcurrentHashMap<>();
public ChannelServiceImpl(ChannelConfig config) {
this.config = config;
}
@Override
public boolean isEnabled() {
return config.isEnabled();
}
@Override
public Channel getDefault() {
return config.getDefault();
}
@Override
public Collection<Channel> getChannels() {
return config.getChannels();
}
@Override
public Channel find(String nameOrAlias) {
return config.find(nameOrAlias);
}
@Override
public Optional<Channel> getActive(Player player) {
String id = activeChannels.get(player.getUniqueId());
if (id == null) {
return Optional.empty();
}
return Optional.ofNullable(config.get(id));
}
@Override
public boolean isInChannel(Player player) {
return activeChannels.containsKey(player.getUniqueId());
}
@Override
public boolean canUse(Player player, Channel channel) {
return !channel.hasPermission() || player.hasPermission(channel.permission());
}
@Override
public boolean join(Player player, Channel channel) {
if (channel == null || !canUse(player, channel)) {
return false;
}
Channel defaultChannel = config.getDefault();
if (defaultChannel != null && channel.id().equals(defaultChannel.id())) {
activeChannels.remove(player.getUniqueId());
return true;
}
activeChannels.put(player.getUniqueId(), channel.id());
return true;
}
@Override
public void leave(Player player) {
activeChannels.remove(player.getUniqueId());
}
}

View file

@ -1,6 +1,13 @@
package com.loki.lochat.core.service;
import com.loki.lochat.compat.Msg;
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 +15,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 +34,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,30 +49,38 @@ 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);
Component senderDisplay = Component.text(sender.getDisplayName());
for (Player p : Bukkit.getOnlinePlayers()) {
if (!isGlobalChatDisabled(p.getUniqueId())) {
p.sendMessage(renderer.render(sender, sender.displayName(), msg, p));
Msg.send(p, renderer.render(sender, senderDisplay, msg, p));
}
}
}
@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;
Component senderDisplay2 = Component.text(sender.getDisplayName());
for (Player p : Bukkit.getOnlinePlayers()) {
if (DistanceUtil.isInRange(sender, p, radius)) {
p.sendMessage(renderer.render(sender, sender.displayName(), msg, p));
Msg.send(p, renderer.render(sender, senderDisplay2, msg, p));
count++;
}
}
if (count <= 1) {
sender.sendMessage(ChatFormatter.parse(
Msg.send(sender, ChatFormatter.parse(
plugin.getConfig().getString("messages.local.nobody-heard",
"<color:#9878C9>Вас никто не услышал — рядом нет игроков</color>")));
}
@ -112,7 +130,38 @@ public class ChatServiceImpl implements ChatService {
FilePersistence.saveYaml(plugin, "data/players.yml", cfg);
}
private Component toComponent(Object message) {
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) {
Msg.send(sender, 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()) {
Msg.send(sender, ChatFormatter.parse(result.blockReason()));
return null;
}
text = result.filteredMessage();
}
String safe = MiniMessage.miniMessage().escapeTags(text);
if (sender.hasPermission("lochat.chat.colors")) {
return ChatFormatter.parse(safe);
}
return Component.text(safe);
}
}

View file

@ -1,9 +1,12 @@
package com.loki.lochat.core.service;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.config.RatConfig;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.persistence.FilePersistence;
import com.loki.lochat.utils.platform.FoliaUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
@ -76,6 +79,9 @@ public class NickServiceImpl implements NickService {
// Устанавливаем ник
nicknames.put(player, nickname);
// Персистим сразу — не теряем ники при падении сервера
FoliaUtil.runAsync(plugin, this::save);
// Обновляем display
Player onlinePlayer = Bukkit.getPlayer(player);
if (onlinePlayer != null) {
@ -89,11 +95,15 @@ public class NickServiceImpl implements NickService {
public void resetNickname(UUID player) {
nicknames.remove(player);
// Персистим сразу
FoliaUtil.runAsync(plugin, this::save);
// Сбрасываем display
Player onlinePlayer = Bukkit.getPlayer(player);
if (onlinePlayer != null) {
onlinePlayer.displayName(Component.text(onlinePlayer.getName()));
onlinePlayer.playerListName(Component.text(onlinePlayer.getName()));
Component reset = Component.text(onlinePlayer.getName());
Msg.setDisplayName(onlinePlayer, reset);
Msg.setPlayerListName(onlinePlayer, reset);
}
}
@ -148,12 +158,13 @@ public class NickServiceImpl implements NickService {
if (nickname.isPresent()) {
// Парсим ник с цветами
Component nickComponent = ChatFormatter.parse(nickname.get());
player.displayName(nickComponent);
player.playerListName(nickComponent);
Msg.setDisplayName(player, nickComponent);
Msg.setPlayerListName(player, nickComponent);
} else {
// Используем обычное имя
player.displayName(Component.text(player.getName()));
player.playerListName(Component.text(player.getName()));
Component reset = Component.text(player.getName());
Msg.setDisplayName(player, reset);
Msg.setPlayerListName(player, reset);
}
}

View file

@ -20,9 +20,8 @@ public class PlayerServiceImpl implements PlayerService {
private final JavaPlugin plugin;
// Cooldown state
private final Map<UUID, Long> globalCooldowns = new ConcurrentHashMap<>();
private final Map<UUID, Long> localCooldowns = new ConcurrentHashMap<>();
// Cooldown state — separate map per chat type (global, local, rp_me, rp_do, rp_try, pm...)
private final Map<String, Map<UUID, Long>> cooldowns = new ConcurrentHashMap<>();
// Statistics state
private final Map<UUID, Long> playerMessages = new ConcurrentHashMap<>();
@ -32,6 +31,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;
@ -73,12 +73,11 @@ public class PlayerServiceImpl implements PlayerService {
@Override
public void removeCooldown(UUID player) {
globalCooldowns.remove(player);
localCooldowns.remove(player);
cooldowns.values().forEach(map -> map.remove(player));
}
private Map<UUID, Long> getCooldownMap(String type) {
return "global".equals(type) ? globalCooldowns : localCooldowns;
return cooldowns.computeIfAbsent(type, k -> new ConcurrentHashMap<>());
}
// ========== Statistics Implementation ==========
@ -113,6 +112,7 @@ public class PlayerServiceImpl implements PlayerService {
@Override
public void saveAll() {
synchronized (statsLock) {
stats.set("global.total-messages",
stats.getLong("global.total-messages", 0) + totalMessages.getAndSet(0));
stats.set("by-type.global-chat",
@ -124,15 +124,18 @@ public class PlayerServiceImpl implements PlayerService {
stats.set("last-updated", System.currentTimeMillis());
FilePersistence.saveYaml(plugin, "data/statistics.yml", stats);
}
}
// ========== Private Methods ==========
private void persistPlayerMessages(UUID uuid, long count) {
synchronized (statsLock) {
String path = "top-players.by-messages." + uuid;
long prev = stats.getLong(path + ".count", 0);
stats.set(path + ".count", prev + count);
stats.set(path + ".last-updated", System.currentTimeMillis());
FilePersistence.saveYaml(plugin, "data/statistics.yml", stats);
}
playerMessages.remove(uuid);
}

View file

@ -1,200 +0,0 @@
package com.loki.lochat.core.service;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.MessagesConfig;
import com.loki.lochat.data.model.BanRecord;
import com.loki.lochat.data.model.WarnEntry;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.format.TimeFormatter;
import com.loki.lochat.utils.persistence.FilePersistence;
import com.loki.lochat.utils.platform.FoliaUtil;
import net.kyori.adventure.text.Component;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Варны и баны в punishments.json
*/
public class PunishmentServiceImpl implements PunishmentService {
private final JavaPlugin plugin;
private final MessagesConfig messages;
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private final Map<UUID, List<WarnEntry>> warns = new ConcurrentHashMap<>();
private final Map<UUID, BanRecord> bans = new ConcurrentHashMap<>();
public PunishmentServiceImpl(JavaPlugin plugin, MessagesConfig messagesConfig) {
this.plugin = plugin;
this.messages = messagesConfig;
load();
}
@Override
public void addWarn(UUID uuid, String playerName, String moderator, String reason, boolean silent) {
WarnEntry entry = new WarnEntry(System.currentTimeMillis(), moderator, reason, silent);
warns.computeIfAbsent(uuid, k -> Collections.synchronizedList(new ArrayList<>())).add(entry);
saveAsync();
}
@Override
public List<WarnEntry> getWarns(UUID uuid) {
List<WarnEntry> list = warns.get(uuid);
if (list == null) {
return List.of();
}
synchronized (list) {
return List.copyOf(list);
}
}
@Override
public int getWarnCount(UUID uuid) {
List<WarnEntry> list = warns.get(uuid);
return list == null ? 0 : list.size();
}
@Override
public void ban(UUID uuid, String playerName, long durationMs, String reason, String bannedBy) {
long until = durationMs <= 0 ? 0 : System.currentTimeMillis() + durationMs;
BanRecord record = new BanRecord(uuid, playerName, reason, bannedBy, until, System.currentTimeMillis());
bans.put(uuid, record);
saveAsync();
}
@Override
public boolean unban(UUID uuid) {
BanRecord removed = bans.remove(uuid);
if (removed != null) {
saveAsync();
return true;
}
return false;
}
@Override
public boolean isBanned(UUID uuid) {
BanRecord b = bans.get(uuid);
if (b == null) {
return false;
}
if (b.isPermanent()) {
return true;
}
if (b.isExpired()) {
bans.remove(uuid, b);
saveAsync();
return false;
}
return true;
}
@Override
public BanRecord getActiveBan(UUID uuid) {
if (!isBanned(uuid)) {
return null;
}
return bans.get(uuid);
}
@Override
public Component buildBanKickMessage(BanRecord ban) {
String reason = ban.reason != null ? ban.reason : messages.getModerationDefaultReason();
String mod = ban.bannedBy != null ? ban.bannedBy : "?";
String duration;
if (ban.isPermanent()) {
duration = messages.getModerationDurationPermanent();
} else {
long left = Math.max(0, ban.until - System.currentTimeMillis());
duration = TimeFormatter.format(left);
}
String template = messages.getBanKickMessage();
String filled = template
.replace("{reason}", reason)
.replace("{moderator}", mod)
.replace("{duration}", duration)
.replace("{player}", ban.playerName != null ? ban.playerName : "?");
return ChatFormatter.parse(filled);
}
@Override
public void save() {
PunishmentSnapshot snap = new PunishmentSnapshot();
for (Map.Entry<UUID, List<WarnEntry>> e : warns.entrySet()) {
List<WarnEntry> list = e.getValue();
synchronized (list) {
snap.getWarns().put(e.getKey().toString(), new ArrayList<>(list));
}
}
for (Map.Entry<UUID, BanRecord> e : bans.entrySet()) {
snap.getBans().put(e.getKey().toString(), e.getValue());
}
FilePersistence.saveJson(plugin, "punishments.json", snap, gson);
}
@Override
public void reload() {
warns.clear();
bans.clear();
load();
}
private void saveAsync() {
FoliaUtil.runAsync(plugin, this::save);
}
private void load() {
PunishmentSnapshot snap = FilePersistence.loadJson(plugin, "punishments.json", PunishmentSnapshot.class, gson);
if (snap == null) {
return;
}
if (snap.getWarns() != null) {
for (Map.Entry<String, List<WarnEntry>> e : snap.getWarns().entrySet()) {
try {
warns.put(UUID.fromString(e.getKey()), Collections.synchronizedList(new ArrayList<>(e.getValue())));
} catch (IllegalArgumentException ignored) {
}
}
}
if (snap.getBans() != null) {
for (Map.Entry<String, BanRecord> e : snap.getBans().entrySet()) {
try {
UUID id = UUID.fromString(e.getKey());
BanRecord br = e.getValue();
if (br != null) {
br.uuid = id;
if (!br.isPermanent() && br.isExpired()) {
continue;
}
bans.put(id, br);
}
} catch (IllegalArgumentException ignored) {
}
}
}
}
private static class PunishmentSnapshot {
private Map<String, List<WarnEntry>> warns = new HashMap<>();
private Map<String, BanRecord> bans = new HashMap<>();
Map<String, List<WarnEntry>> getWarns() {
return warns;
}
Map<String, BanRecord> getBans() {
return bans;
}
}
}

View file

@ -1,7 +1,10 @@
package com.loki.lochat.core.service.messaging;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.api.service.pm.PrivateMessageService;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.player.PlayerUtil;
@ -23,10 +26,12 @@ public class PrivateMessageServiceImpl implements PrivateMessageService {
private final Map<UUID, UUID> lastConversation = new ConcurrentHashMap<>();
private final LoChat plugin;
private final PlayerService playerService;
private MessagingService messagingService;
public PrivateMessageServiceImpl(LoChat plugin) {
public PrivateMessageServiceImpl(LoChat plugin, PlayerService playerService) {
this.plugin = plugin;
this.playerService = playerService;
}
public void init(MessagingService messagingService) {
@ -56,9 +61,22 @@ public class PrivateMessageServiceImpl implements PrivateMessageService {
@Override
public void sendPrivateMessage(CommandSender sender, Player target, String message) {
Player playerSender = (Player) sender;
sender.sendMessage(ChatFormatter.formatPmSentNew(
int cooldown = plugin.getConfig().getInt("chat.pm.cooldown", 2);
if (cooldown > 0 && !playerSender.hasPermission("chat.bypass.cooldown")
&& playerService.isOnCooldown(playerSender.getUniqueId(), "pm", cooldown)) {
int remaining = playerService.getRemainingCooldown(
playerSender.getUniqueId(), "pm", cooldown);
String cooldownMessage = plugin.getConfigManager().getMessagesConfig()
.getCooldownMessage().replace("{remaining}", String.valueOf(remaining));
Msg.send(sender, ChatFormatter.parse(cooldownMessage));
return;
}
playerService.setCooldown(playerSender.getUniqueId(), "pm");
Msg.send(sender, ChatFormatter.formatPmSentNew(
plugin.getMessageConfig().getPmFormatSent(), playerSender, target, message));
target.sendMessage(ChatFormatter.formatPmReceivedNew(
Msg.send(target, ChatFormatter.formatPmReceivedNew(
plugin.getMessageConfig().getPmFormatReceived(), playerSender, target, message));
if (plugin.getConfigManager().isPmSoundEnabled()) {
Sound sound = PlayerUtil.parseSound(plugin.getConfigManager().getPmSoundType(), null);

View file

@ -1,5 +1,7 @@
package com.loki.lochat.core.service.messaging;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.config.MessageConfig;
import com.loki.lochat.utils.format.ChatFormatter;
@ -46,7 +48,7 @@ public class SpyService {
for (UUID spyUuid : spyEnabled) {
Player spy = Bukkit.getPlayer(spyUuid);
if (spy != null && spy.isOnline() && !spy.equals(sender) && !spy.equals(receiver)) {
spy.sendMessage(ChatFormatter.parse(format
Msg.send(spy, ChatFormatter.parse(format
.replace("{sender}", sender.getName())
.replace("{receiver}", receiver.getName())
.replace("{message}", message)));
@ -67,7 +69,7 @@ public class SpyService {
for (UUID spyUuid : spyEnabled) {
Player spy = Bukkit.getPlayer(spyUuid);
if (spy != null && spy.isOnline() && !spy.equals(sender)) {
spy.sendMessage(ChatFormatter.parse(format
Msg.send(spy, ChatFormatter.parse(format
.replace("{type}", chatType)
.replace("{sender}", sender.getName())
.replace("{message}", plainMessage)));

View file

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

View file

@ -7,19 +7,30 @@ 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;
import java.util.logging.Level;
import java.util.logging.Logger;
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 +97,35 @@ public class MuteHistoryManager {
}
public void save() {
try (FileWriter writer = new FileWriter(historyFile)) {
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();
Logger.getLogger(MuteHistoryManager.class.getName()).log(Level.WARNING, "Failed to save mute history", e);
} 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);
}
}
@ -106,7 +142,7 @@ public class MuteHistoryManager {
history.putAll(loaded);
}
} catch (IOException e) {
e.printStackTrace();
Logger.getLogger(MuteHistoryManager.class.getName()).log(Level.WARNING, "Failed to load mute history", e);
}
}
}

View file

@ -1,39 +0,0 @@
package com.loki.lochat.data.model;
import java.util.UUID;
/**
* Активная блокировка
*/
public class BanRecord {
public UUID uuid;
public String playerName;
public String reason;
public String bannedBy;
/** 0 = перманент; иначе timestamp окончания бана */
public long until;
public long createdAt;
public BanRecord() {
}
public BanRecord(UUID uuid, String playerName, String reason, String bannedBy, long until, long createdAt) {
this.uuid = uuid;
this.playerName = playerName;
this.reason = reason;
this.bannedBy = bannedBy;
this.until = until;
this.createdAt = createdAt;
}
public boolean isPermanent() {
return until == 0;
}
public boolean isExpired() {
if (isPermanent()) {
return false;
}
return System.currentTimeMillis() >= until;
}
}

View file

@ -1,21 +0,0 @@
package com.loki.lochat.data.model;
/**
* Запись о варне (для JSON)
*/
public class WarnEntry {
public long time;
public String moderator;
public String reason;
public boolean silent;
public WarnEntry() {
}
public WarnEntry(long time, String moderator, String reason, boolean silent) {
this.time = time;
this.moderator = moderator;
this.reason = reason;
this.silent = silent;
}
}

View file

@ -95,6 +95,7 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter {
sender.sendMessage("§e/aprefix updateall §7— обновить display names всех игроков");
}
@SuppressWarnings("deprecation")
private void handleInfo(CommandSender sender, String playerName, GradientMessages msg) {
OfflinePlayer target = Bukkit.getOfflinePlayer(playerName);
if (!target.hasPlayedBefore() && !target.isOnline()) {

View file

@ -29,6 +29,7 @@ public class ColorCommandHandler {
/**
* Установить цвет игроку
*/
@SuppressWarnings("deprecation")
public void handleSetColor(CommandSender sender, String[] args, GradientMessages msg) {
if (args.length < 3) {
sender.sendMessage("§cИспользование: /aprefix setcolor <игрок> <цвет1> [цвет2] ...");
@ -69,6 +70,7 @@ public class ColorCommandHandler {
/**
* Включить/выключить цвет
*/
@SuppressWarnings("deprecation")
public void handleColorToggle(CommandSender sender, String[] args, boolean enable, GradientMessages msg) {
if (args.length < 2) {
sender.sendMessage("§cИспользование: /aprefix " + (enable ? "coloron" : "coloroff") + " <игрок>");
@ -93,6 +95,7 @@ public class ColorCommandHandler {
/**
* Сбросить цвет
*/
@SuppressWarnings("deprecation")
public void handleResetColor(CommandSender sender, String[] args, GradientMessages msg) {
if (args.length < 2) {
sender.sendMessage("§cИспользование: /aprefix resetcolor <игрок>");

View file

@ -25,6 +25,7 @@ public class PrefixCommandHandler {
/**
* Установить префикс игроку
*/
@SuppressWarnings("deprecation")
public void handleSetPrefix(CommandSender sender, String[] args, GradientMessages msg) {
if (args.length < 3) {
sender.sendMessage("§cИспользование: /aprefix setprefix <игрок> <префикс>");
@ -65,6 +66,7 @@ public class PrefixCommandHandler {
/**
* Включить/выключить префикс
*/
@SuppressWarnings("deprecation")
public void handlePrefixToggle(CommandSender sender, String[] args, boolean enable, GradientMessages msg) {
if (args.length < 2) {
sender.sendMessage("§cИспользование: /aprefix " + (enable ? "prefixon" : "prefixoff") + " <игрок>");
@ -89,6 +91,7 @@ public class PrefixCommandHandler {
/**
* Сбросить префикс
*/
@SuppressWarnings("deprecation")
public void handleResetPrefix(CommandSender sender, String[] args, GradientMessages msg) {
if (args.length < 2) {
sender.sendMessage("§cИспользование: /aprefix resetprefix <игрок>");

View file

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

View file

@ -1,5 +1,7 @@
package com.loki.lochat.gradient.config;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.utils.format.ChatFormatter;
import net.kyori.adventure.text.Component;
@ -35,12 +37,15 @@ public class GradientMessages {
}
messages = YamlConfiguration.loadConfiguration(messagesFile);
InputStream defStream = plugin.getResource("messages.yml");
try (InputStream defStream = plugin.getResource("messages.yml")) {
if (defStream != null) {
YamlConfiguration defConfig = YamlConfiguration.loadConfiguration(
new InputStreamReader(defStream, StandardCharsets.UTF_8));
messages.setDefaults(defConfig);
}
} catch (java.io.IOException e) {
plugin.getLogger().warning("Failed to load default messages.yml: " + e.getMessage());
}
}
public void reload() {
@ -86,13 +91,13 @@ public class GradientMessages {
* Отправляет сообщение игроку
*/
public void send(CommandSender sender, String key) {
sender.sendMessage(getComponent(key));
Msg.send(sender, getComponent(key));
}
/**
* Отправляет сообщение игроку с заменами
*/
public void send(CommandSender sender, String key, String... replacements) {
sender.sendMessage(getComponent(key, replacements));
Msg.send(sender, getComponent(key, replacements));
}
}

View file

@ -104,7 +104,7 @@ public class GradientDataManager {
"SELECT prefix, colors, color_enabled, prefix_enabled, "
+ "prefix_purchased, last_color_change, last_prefix_change FROM gradient_data WHERE uuid = ?")) {
stmt.setString(1, uuid.toString());
ResultSet rs = stmt.executeQuery();
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
data.setPrefix(rs.getString("prefix"));
String colorsStr = rs.getString("colors");
@ -117,6 +117,7 @@ public class GradientDataManager {
data.setLastColorChange(rs.getLong("last_color_change"));
data.setLastPrefixChange(rs.getLong("last_prefix_change"));
}
}
} catch (SQLException e) {
plugin.getLogger().warning("Ошибка загрузки gradient данных: " + e.getMessage());
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.gradient.gui;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.config.RatConfig;
import com.loki.lochat.gradient.GradientModule;
import com.loki.lochat.gradient.util.GradientUtil;
@ -55,7 +57,7 @@ public final class GradientConfirmGUI implements InventoryHolder {
return;
}
this.inventory = Bukkit.createInventory(this, 27,
Component.text("Подтверждение", NamedTextColor.DARK_GRAY));
Msg.inventoryTitle(Component.text("Подтверждение", NamedTextColor.DARK_GRAY)));
setupItems();
}
@ -72,7 +74,7 @@ public final class GradientConfirmGUI implements InventoryHolder {
String preview = buildPreview();
// Для GUI используем LegacyComponentSerializer с поддержкой HEX
Component previewComponent = SERIALIZER.deserialize(preview);
skullMeta.displayName(previewComponent.decoration(TextDecoration.ITALIC, false));
Msg.itemName(skullMeta, previewComponent.decoration(TextDecoration.ITALIC, false));
List<Component> lore = new ArrayList<>();
lore.add(Component.empty());
@ -83,7 +85,7 @@ public final class GradientConfirmGUI implements InventoryHolder {
.append(Component.text(price + " PP", NamedTextColor.GOLD))
.decoration(TextDecoration.ITALIC, false));
}
skullMeta.lore(lore);
Msg.itemLore(skullMeta, lore);
head.setItemMeta(skullMeta);
inventory.setItem(RatConfig.GUI_PREVIEW_SLOT, head);
@ -93,7 +95,7 @@ public final class GradientConfirmGUI implements InventoryHolder {
confirmLore.add(Component.empty());
confirmLore.add(Component.text("Нажмите для подтверждения", NamedTextColor.GREEN)
.decoration(TextDecoration.ITALIC, false));
confirmMeta.lore(confirmLore);
Msg.itemLore(confirmMeta, confirmLore);
confirm.setItemMeta(confirmMeta);
inventory.setItem(RatConfig.GUI_CONFIRM_SLOT, confirm);
@ -103,7 +105,7 @@ public final class GradientConfirmGUI implements InventoryHolder {
cancelLore.add(Component.empty());
cancelLore.add(Component.text("Нажмите для отмены", NamedTextColor.RED)
.decoration(TextDecoration.ITALIC, false));
cancelMeta.lore(cancelLore);
Msg.itemLore(cancelMeta, cancelLore);
cancel.setItemMeta(cancelMeta);
inventory.setItem(RatConfig.GUI_CANCEL_SLOT, cancel);
}
@ -187,7 +189,7 @@ public final class GradientConfirmGUI implements InventoryHolder {
private ItemStack createItem(Material material, String name) {
ItemStack item = new ItemStack(material);
ItemMeta meta = item.getItemMeta();
meta.displayName(LegacyComponentSerializer.legacySection().deserialize(name)
Msg.itemName(meta, LegacyComponentSerializer.legacySection().deserialize(name)
.decoration(TextDecoration.ITALIC, false));
item.setItemMeta(meta);
return item;

View file

@ -1,5 +1,7 @@
package com.loki.lochat.gradient.util;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.gradient.GradientModule;
import com.loki.lochat.gradient.config.GradientConfig;
import com.loki.lochat.gradient.data.GradientPlayerData;
@ -32,8 +34,8 @@ public final class DisplayNameUtil {
// Конвертируем в legacy формат для display name
displayName = convertMiniMessageToLegacy(displayName);
var component = SERIALIZER.deserialize(displayName);
player.displayName(component);
player.playerListName(component);
Msg.setDisplayName(player, component);
Msg.setPlayerListName(player, component);
}
/**

View file

@ -1,39 +0,0 @@
package com.loki.lochat.integrations;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import org.bukkit.entity.Player;
/**
* Заглушка для совместимости с LibertyBans
* Теперь используется встроенная система мутов LoChat
*/
public class LibertyBansHook {
private final MuteService muteService;
public LibertyBansHook(LoChat plugin) {
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
// Проверяем наличие LibertyBans
if (plugin.getServer().getPluginManager().getPlugin("LibertyBans") != null) {
plugin.getLogger().info("LibertyBans обнаружен. Используйте встроенную систему мутов LoChat (/lmute, /lunmute)");
}
}
/**
* Проверяет, замучен ли игрок через встроенную систему LoChat
*/
public boolean isMuted(Player player) {
return muteService.isMuted(player.getUniqueId());
}
public boolean isEnabled() {
return true; // Всегда используем встроенную систему
}
public boolean isPluginPresent() {
return true;
}
}

View file

@ -40,7 +40,7 @@ public class PlaceholderAPIHook extends PlaceholderExpansion {
@Override
public @NotNull String getVersion() {
return plugin.getPluginMeta().getVersion();
return plugin.getDescription().getVersion();
}
@Override

View file

@ -25,11 +25,9 @@ public class DiscordConfig {
File configFile = new File(configFolder, "discord.yml");
if (!configFile.exists()) {
try {
java.io.InputStream in = plugin.getResource("config/discord.yml");
try (java.io.InputStream in = plugin.getResource("config/discord.yml")) {
if (in != null) {
java.nio.file.Files.copy(in, configFile.toPath());
in.close();
}
} catch (Exception e) {
plugin.getLogger().warning("Failed to copy config/discord.yml: " + e.getMessage());
@ -52,7 +50,7 @@ public class DiscordConfig {
}
public String getAvatarUrl() {
return config.getString("webhook.avatar_url", "");
return config.getString("webhook.avatar-url", "");
}
public int getTimeout() {
@ -60,11 +58,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 +82,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 +94,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 +119,14 @@ public class DiscordConfig {
}
public int getMinMessageLength() {
return config.getInt("filter.min_message_length", 1);
return config.getInt("filter.min-message-length", 1);
}
public int getMaxMessageLength() {
return config.getInt("filter.max_message_length", 2000);
return config.getInt("filter.max-message-length", 2000);
}
public boolean sanitizeMentions() {
return config.getBoolean("filter.sanitize_mentions", true);
return config.getBoolean("filter.sanitize-mentions", true);
}
}

View file

@ -1,49 +1,65 @@
package com.loki.lochat.listener;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChannelService;
import com.loki.lochat.api.service.MessageService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.channel.Channel;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.core.filter.AdvancedMessageFilter;
import com.loki.lochat.core.filter.FilterResult;
import com.loki.lochat.renderer.EnhancedChatRenderer;
import com.loki.lochat.translate.TranslationService;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.format.LuckPermsAffixes;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.plugin.java.JavaPlugin;
import io.papermc.paper.event.player.AsyncChatEvent;
import java.util.HashSet;
import java.util.Set;
public class ChatEventListener implements Listener {
private final JavaPlugin plugin;
private final MessageService messageService;
private final PlayerService playerService;
private final AdvancedMessageFilter advancedFilter;
private final ChannelService channelService;
public ChatEventListener(JavaPlugin plugin, MessageService messageService, PlayerService playerService,
AdvancedMessageFilter filter) {
AdvancedMessageFilter filter, ChannelService channelService) {
this.plugin = plugin;
this.messageService = messageService;
this.playerService = playerService;
this.advancedFilter = filter;
this.channelService = channelService;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onChat(AsyncChatEvent event) {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onChat(AsyncPlayerChatEvent event) {
Player sender = event.getPlayer();
String rawMessage = PlainTextComponentSerializer.plainText().serialize(event.message());
String rawMessage = event.getMessage();
LoChat loChat = (LoChat) plugin;
// Система каналов: если игрок в присоединённом канале — маршрутизируем туда
Channel active = channelService.isEnabled() ? channelService.getActive(sender).orElse(null) : null;
if (active != null) {
handleChannelChat(event, sender, active, rawMessage);
return;
}
// Определяем режим чата (глобальный/локальный)
ChatMode mode = determineChatMode(loChat, rawMessage);
if (mode == null) {
event.setCancelled(true);
sender.sendMessage(ChatFormatter.parse("&#CF6679Этот режим чата отключен!"));
Msg.send(sender, ChatFormatter.parse("&#CF6679Этот режим чата отключен!"));
return;
}
@ -51,7 +67,8 @@ public class ChatEventListener implements Listener {
boolean isGlobal = mode.isGlobal;
// Проверки: мут, кулдаун, фильтры
if (!processFilters(event, sender, plainMessage, isGlobal)) {
Component messageComponent = processFilters(event, sender, plainMessage, isGlobal);
if (messageComponent == null) {
return;
}
@ -60,12 +77,76 @@ public class ChatEventListener implements Listener {
handleLocalChat(event, sender, loChat);
}
// Рендеринг и интеграции
event.renderer(new EnhancedChatRenderer(plugin, isGlobal));
// Рендеринг и доставка
EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, isGlobal);
Component displayName = buildDisplayName(sender);
Set<Player> recipients = new HashSet<>(event.getRecipients());
event.setCancelled(true);
prefetchTranslation(sender, messageComponent, recipients);
for (Player viewer : recipients) {
Component rendered = renderer.render(sender, displayName, messageComponent, viewer);
Msg.send(viewer, rendered);
}
Component consoleRendered = renderer.render(sender, displayName, messageComponent, null);
Bukkit.getConsoleSender().sendMessage(Msg.plain(consoleRendered));
loChat.getDiscordIntegration().sendChatMessage(sender, plainMessage, isGlobal);
recordStatistics(loChat, sender, isGlobal);
}
/**
* Маршрутизация сообщения в присоединённый (не-дефолтный) канал.
*/
private void handleChannelChat(AsyncPlayerChatEvent event, Player sender, Channel channel, String rawMessage) {
String plainMessage = rawMessage.stripLeading();
// Кулдаун канала
int cooldown = channel.cooldown();
if (cooldown > 0 && !sender.hasPermission("chat.bypass.cooldown")) {
String cooldownType = "channel_" + channel.id();
if (playerService.isOnCooldown(sender.getUniqueId(), cooldownType, cooldown)) {
int remaining = playerService.getRemainingCooldown(sender.getUniqueId(), cooldownType, cooldown);
String msg = ((LoChat) plugin).getConfigManager().getMessagesConfig().getCooldownMessage()
.replace("{remaining}", String.valueOf(remaining));
event.setCancelled(true);
Msg.send(sender, ChatFormatter.parse(msg));
return;
}
playerService.setCooldown(sender.getUniqueId(), cooldownType);
}
Component messageComponent = processFilters(event, sender, plainMessage, channel.isGlobalScope());
if (messageComponent == null) {
return;
}
// Радиус локального канала
if (channel.isLocalScope()) {
int radius = channel.radius();
event.getRecipients().removeIf(p -> {
try {
return !com.loki.lochat.utils.player.DistanceUtil.isInRange(sender, p, radius);
} catch (Exception e) {
return false;
}
});
}
EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, channel);
Component displayName = buildDisplayName(sender);
Set<Player> recipients = new HashSet<>(event.getRecipients());
event.setCancelled(true);
prefetchTranslation(sender, messageComponent, recipients);
for (Player viewer : recipients) {
Component rendered = renderer.render(sender, displayName, messageComponent, viewer);
Msg.send(viewer, rendered);
}
Component consoleRendered = renderer.render(sender, displayName, messageComponent, null);
Bukkit.getConsoleSender().sendMessage(Msg.plain(consoleRendered));
LoChat loChat = (LoChat) plugin;
loChat.getDiscordIntegration().sendChatMessage(sender, plainMessage, channel.isGlobalScope());
playerService.recordMessage(sender.getUniqueId(), "channel_" + channel.id());
}
private ChatMode determineChatMode(LoChat loChat, String rawMessage) {
boolean localEnabled = loChat.getConfigManager().isLocalEnabled();
boolean isGlobal;
@ -90,21 +171,21 @@ public class ChatEventListener implements Listener {
return new ChatMode(isGlobal, message);
}
private boolean processFilters(AsyncChatEvent event, Player sender, String plainMessage, boolean isGlobal) {
private Component processFilters(AsyncPlayerChatEvent event, Player sender, String plainMessage, boolean isGlobal) {
LoChat loChat = (LoChat) plugin;
// Мут, кулдаун
if (!messageService.processMessage(sender, plainMessage)) {
event.setCancelled(true);
return false;
return null;
}
// Капс, реклама, повторы
FilterResult filterResult = advancedFilter.filterMessage(sender, plainMessage);
if (!filterResult.allowed()) {
event.setCancelled(true);
sender.sendMessage(ChatFormatter.parse(filterResult.blockReason()));
return false;
Msg.send(sender, ChatFormatter.parse(filterResult.blockReason()));
return null;
}
String filteredMessage = filterResult.filteredMessage();
@ -120,17 +201,13 @@ public class ChatEventListener implements Listener {
messageComponent = ChatFormatter.parseWithDefaultMessageColor(filteredMessage, defaultColor);
}
event.message(messageComponent);
return true;
return messageComponent;
}
private void handleLocalChat(AsyncChatEvent event, Player sender, LoChat loChat) {
private void handleLocalChat(AsyncPlayerChatEvent event, Player sender, LoChat loChat) {
int radius = loChat.getConfigManager().getAppearanceConfig().getLocalRadius();
event.viewers().removeIf(v -> {
if (!(v instanceof Player p)) {
return false;
}
event.getRecipients().removeIf(p -> {
try {
return !com.loki.lochat.utils.player.DistanceUtil.isInRange(sender, p, radius);
} catch (Exception e) {
@ -138,15 +215,49 @@ public class ChatEventListener implements Listener {
}
});
long recipients = event.viewers().stream()
.filter(v -> v instanceof Player p && !p.equals(sender))
long recipients = event.getRecipients().stream()
.filter(p -> !p.equals(sender))
.count();
if (recipients == 0) {
String msg = loChat.getConfigManager().getMessagesConfig().getNobodyHeard();
sender.getScheduler().run(plugin, t ->
sender.sendMessage(ChatFormatter.parse(msg)), null
);
Bukkit.getScheduler().runTask(plugin, () -> Msg.send(sender, ChatFormatter.parse(msg)));
}
}
private Component buildDisplayName(Player sender) {
String legacyName = sender.getDisplayName();
if (legacyName == null || legacyName.isBlank()) {
return Component.text(sender.getName());
}
// LuckPerms prefix/suffix are added separately ({player_prefix}/{player_suffix}), do not show them twice
legacyName = LuckPermsAffixes.withoutAffixes(legacyName, LuckPermsAffixes.prefix(sender),
LuckPermsAffixes.suffix(sender));
return ChatFormatter.parse(legacyName);
}
/** Translates the message for every viewer language before rendering (runs off the main thread only). */
private void prefetchTranslation(Player sender, Component messageComponent, Set<Player> recipients) {
try {
LoChat loChat = (LoChat) plugin;
var cfg = loChat.getConfigManager();
TranslationService ts = loChat.getTranslationService();
if (!cfg.isTranslationEnabled() || !ts.isEnabled() || Bukkit.isPrimaryThread()
|| "actionbar".equalsIgnoreCase(cfg.getTranslationMode())
|| sender.hasPermission(cfg.getTranslationBypassPermission())) {
return;
}
Set<String> languages = new HashSet<>();
for (Player viewer : recipients) {
if (!viewer.equals(sender)) {
languages.add(TranslationService.localeToLanguageCode(viewer.getLocale()));
}
}
if (!languages.isEmpty()) {
ts.prefetch(Msg.plain(messageComponent), languages, cfg.getTranslationTimeoutMs());
}
} catch (Exception e) {
plugin.getLogger().fine("Translation prefetch failed: " + e.getMessage());
}
}

View file

@ -2,8 +2,6 @@ package com.loki.lochat.listener;
import com.loki.lochat.integrations.DiscordIntegration;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
@ -12,8 +10,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 +20,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);
}
/**
* Обработка входа игрока
*/
@ -83,9 +56,8 @@ public class DiscordEventListener implements Listener {
}
Player player = event.getEntity();
String deathMessage = event.deathMessage() != null ?
PlainTextComponentSerializer.plainText().serialize(event.deathMessage()) :
"умер";
String raw = event.getDeathMessage();
String deathMessage = raw != null ? raw : "умер";
discord.sendPlayerDeath(player, deathMessage);
}

View file

@ -1,33 +0,0 @@
package com.loki.lochat.listener;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.data.model.BanRecord;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
/**
* Блокировка входа забаненным игрокам
*/
public class ModerationListener implements Listener {
private final PunishmentService punishmentService;
public ModerationListener(PunishmentService punishmentService) {
this.punishmentService = punishmentService;
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPreLogin(AsyncPlayerPreLoginEvent event) {
if (!punishmentService.isBanned(event.getUniqueId())) {
return;
}
BanRecord ban = punishmentService.getActiveBan(event.getUniqueId());
if (ban == null) {
return;
}
event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, punishmentService.buildBanKickMessage(ban));
}
}

View file

@ -4,12 +4,11 @@ import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.config.ConfigManager;
import com.loki.lochat.core.filter.AdvancedMessageFilter;
import com.loki.lochat.utils.format.ChatFormatter;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
@ -24,7 +23,6 @@ import org.bukkit.inventory.ItemStack;
* Слушатель событий игрока
*/
public class PlayerEventListener implements Listener {
private final LoChat plugin;
private final PlayerService playerService;
private final MessagingService messagingService;
private final NickService nickService;
@ -33,7 +31,6 @@ public class PlayerEventListener implements Listener {
public PlayerEventListener(LoChat plugin, PlayerService playerService, MessagingService messagingService,
NickService nickService, AdvancedMessageFilter advancedFilter) {
this.plugin = plugin;
this.playerService = playerService;
this.messagingService = messagingService;
this.nickService = nickService;
@ -52,7 +49,8 @@ public class PlayerEventListener implements Listener {
if (configManager.isJoinMessageEnabled()) {
String format = configManager.getJoinMessageFormat();
String message = format.replace("{player}", player.getName());
event.joinMessage(ChatFormatter.parse(message));
event.setJoinMessage(null);
Msg.broadcast(ChatFormatter.parse(message));
}
}
@ -72,7 +70,8 @@ public class PlayerEventListener implements Listener {
if (configManager.isQuitMessageEnabled()) {
String format = configManager.getQuitMessageFormat();
String message = format.replace("{player}", player.getName());
event.quitMessage(ChatFormatter.parse(message));
event.setQuitMessage(null);
Msg.broadcast(ChatFormatter.parse(message));
}
}
@ -82,7 +81,7 @@ public class PlayerEventListener implements Listener {
return;
}
Player player = event.getPlayer();
Player player = event.getEntity();
Player killer = player.getKiller();
String message;
@ -98,8 +97,8 @@ public class PlayerEventListener implements Listener {
.replace("{weapon}", weaponName);
} else {
// Проверяем убийцу-моба
Entity lastDamager = player.getLastDamageCause() != null ?
player.getLastDamageCause().getEntity() : null;
Entity lastDamager = player.getLastDamageCause() != null
? player.getLastDamageCause().getEntity() : null;
if (lastDamager instanceof LivingEntity mob && !(lastDamager instanceof Player)) {
String mobName = getMobName(mob);
@ -108,15 +107,15 @@ public class PlayerEventListener implements Listener {
.replace("{killer}", mobName);
} else {
// Обычная смерть
Component deathMsg = event.deathMessage();
String deathText = deathMsg != null ? ChatFormatter.toPlain(deathMsg) : "умер";
String deathMsg = event.getDeathMessage();
String deathText = deathMsg != null ? ChatFormatter.toPlain(ChatFormatter.parse(deathMsg)) : "умер";
message = configManager.getDeathDefaultFormat()
.replace("{player}", player.getName())
.replace("{death_message}", deathText);
}
}
event.deathMessage(ChatFormatter.parse(message));
event.setDeathMessage(Msg.legacy(ChatFormatter.parse(message)));
}
private String getWeaponName(ItemStack item) {
@ -125,15 +124,19 @@ public class PlayerEventListener implements Listener {
}
if (item.hasItemMeta() && item.getItemMeta().hasDisplayName()) {
return ChatFormatter.toPlain(item.getItemMeta().displayName());
String dn = item.getItemMeta().getDisplayName();
if (dn != null) {
return ChatFormatter.toPlain(ChatFormatter.parse(dn));
}
}
return translateMaterial(item.getType().name());
}
private String getMobName(LivingEntity mob) {
if (mob.customName() != null) {
return ChatFormatter.toPlain(mob.customName());
String cn = mob.getCustomName();
if (cn != null) {
return ChatFormatter.toPlain(ChatFormatter.parse(cn));
}
return translateEntityType(mob.getType().name());
}

View file

@ -1,8 +1,9 @@
package com.loki.lochat.managers;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.platform.FoliaUtil;
import net.kyori.adventure.text.Component;
@ -18,16 +19,13 @@ import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
public class AutoMessageManager {
private final LoChat plugin;
private final Random random = new Random();
private final AtomicInteger currentIndex = new AtomicInteger(0);
// ✅ FIX: Сохраняем ссылку на задачу для отмены
private Object scheduledTask; // BukkitTask или ScheduledTask
private BukkitTask scheduledTask;
private Map<String, List<String>> messages;
private List<String> messageOrder;
@ -44,11 +42,11 @@ public class AutoMessageManager {
// Настройки
prefix = plugin.getConfigManager().getString("prefix", "&#00D4FF[LoChat]");
mode = "sequential"; // Всегда последовательно
mode = config.getBoolean("automessages.random", false) ? "random" : "sequential";
// Загружаем сообщения из config.yml
messages = new LinkedHashMap<>();
ConfigurationSection msgSection = config.getConfigurationSection("automessages-list");
ConfigurationSection msgSection = config.getConfigurationSection("automessages.messages");
if (msgSection != null) {
for (String key : msgSection.getKeys(false)) {
@ -90,37 +88,19 @@ public class AutoMessageManager {
int intervalSeconds = plugin.getConfigManager().getAutoMessagesInterval();
long intervalTicks = intervalSeconds * 20L;
// Используем FoliaUtil для совместимости с Paper и Folia
if (FoliaUtil.isFolia()) {
long delayMs = intervalTicks * 50;
long periodMs = intervalTicks * 50;
scheduledTask = Bukkit.getAsyncScheduler().runAtFixedRate(
plugin,
task -> broadcastNextMessage(),
delayMs,
periodMs,
java.util.concurrent.TimeUnit.MILLISECONDS
);
} else {
scheduledTask = Bukkit.getScheduler().runTaskTimerAsynchronously(
plugin,
this::broadcastNextMessage,
intervalTicks,
intervalTicks
);
}
plugin.getLogger().info("Автосообщения запущены (интервал: " + intervalSeconds + " сек, режим: " + mode + ")");
}
public void stop() {
// ✅ FIX: Отменяем scheduled task
if (scheduledTask != null) {
if (scheduledTask instanceof ScheduledTask) {
((ScheduledTask) scheduledTask).cancel();
} else if (scheduledTask instanceof BukkitTask) {
((BukkitTask) scheduledTask).cancel();
}
scheduledTask.cancel();
scheduledTask = null;
}
}
@ -159,7 +139,7 @@ public class AutoMessageManager {
}
for (var player : Bukkit.getOnlinePlayers()) {
player.sendMessage(formatted);
Msg.send(player, formatted);
}
}
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.managers;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.LoChat;
import com.loki.lochat.commands.CustomCommand;
import com.loki.lochat.managers.commands.MessageSender;
@ -17,6 +19,7 @@ import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
public class CustomCommandManager {
@ -66,9 +69,29 @@ public class CustomCommandManager {
plugin.getLogger().info("Загружено " + commands.size() + " кастомных команд");
}
private CommandMap getCommandMap() {
try {
java.lang.reflect.Method m = Bukkit.getServer().getClass().getMethod("getCommandMap");
return (CommandMap) m.invoke(Bukkit.getServer());
} catch (Exception ignored) {
}
try {
java.lang.reflect.Method m2 = Bukkit.getPluginManager().getClass().getMethod("getCommandMap");
return (CommandMap) m2.invoke(Bukkit.getPluginManager());
} catch (Exception ignored) {
}
try {
java.lang.reflect.Field f = Bukkit.getPluginManager().getClass().getDeclaredField("commandMap");
f.setAccessible(true);
return (CommandMap) f.get(Bukkit.getPluginManager());
} catch (Exception ex) {
throw new RuntimeException("Unable to get CommandMap", ex);
}
}
private void registerCommand(CustomCommandData data) {
try {
CommandMap commandMap = Bukkit.getCommandMap();
CommandMap commandMap = getCommandMap();
CustomCommand command = new CustomCommand(data.name(), this);
commandMap.register("lochat", command);
@ -79,8 +102,7 @@ public class CustomCommandManager {
}
} catch (Exception e) {
plugin.getLogger().severe("Ошибка регистрации команды " + data.name() + ": " + e.getMessage());
e.printStackTrace();
plugin.getLogger().log(Level.WARNING, "Ошибка регистрации команды " + data.name() + ": " + e.getMessage(), e);
}
}
@ -108,7 +130,7 @@ public class CustomCommandManager {
}
if (data.permission() != null && !player.hasPermission(data.permission())) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getNoPermission()));
Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getNoPermission()));
return true;
}
@ -126,7 +148,7 @@ public class CustomCommandManager {
private void sendBungeeCommand(Player player, CustomCommandData data) {
if (!data.message().isEmpty()) {
player.sendMessage(ChatFormatter.parse(data.message()));
Msg.send(player, ChatFormatter.parse(data.message()));
}
String serverName = getServerFromConfig(data);
@ -157,7 +179,7 @@ public class CustomCommandManager {
player.sendPluginMessage(plugin, "BungeeCord", b.toByteArray());
} catch (Exception e) {
plugin.getLogger().severe("Ошибка подключения к серверу " + serverName + ": " + e.getMessage());
player.sendMessage(ChatFormatter.parse("<#F44336>❌ Ошибка подключения к серверу"));
Msg.send(player, ChatFormatter.parse("<#F44336>❌ Ошибка подключения к серверу"));
}
}

View file

@ -1,5 +1,7 @@
package com.loki.lochat.managers.commands;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.utils.format.ChatFormatter;
import net.kyori.adventure.text.Component;
@ -19,13 +21,13 @@ public class MessageSender {
Component formatted = ChatFormatter.parse(message);
switch (target.toLowerCase()) {
case "player" -> sender.sendMessage(formatted);
case "broadcast" -> Bukkit.broadcast(formatted);
case "server" -> Bukkit.getOnlinePlayers().forEach(p -> p.sendMessage(formatted));
case "player" -> Msg.send(sender, formatted);
case "broadcast" -> Msg.broadcast(formatted);
case "server" -> Bukkit.getOnlinePlayers().forEach(p -> Msg.send(p, formatted));
default -> {
Player targetPlayer = Bukkit.getPlayer(target);
if (targetPlayer != null && targetPlayer.isOnline()) {
targetPlayer.sendMessage(formatted);
Msg.send(targetPlayer, formatted);
}
}
}
@ -39,23 +41,19 @@ public class MessageSender {
Component title = ChatFormatter.parse(titleText);
Component subtitle = ChatFormatter.parse(subtitleText);
Title titleObj = Title.title(
title,
subtitle,
Title.Times.times(
Title.Times times = Title.Times.times(
Duration.ofMillis(500),
Duration.ofMillis(3000),
Duration.ofMillis(500)
)
);
switch (target.toLowerCase()) {
case "player" -> sender.showTitle(titleObj);
case "broadcast", "server" -> Bukkit.getOnlinePlayers().forEach(p -> p.showTitle(titleObj));
case "player" -> Msg.title(sender, title, subtitle, times);
case "broadcast", "server" -> Bukkit.getOnlinePlayers().forEach(p -> Msg.title(p, title, subtitle, times));
default -> {
Player targetPlayer = Bukkit.getPlayer(target);
if (targetPlayer != null && targetPlayer.isOnline()) {
targetPlayer.showTitle(titleObj);
Msg.title(targetPlayer, title, subtitle, times);
}
}
}
@ -65,12 +63,12 @@ public class MessageSender {
Component formatted = ChatFormatter.parse(message);
switch (target.toLowerCase()) {
case "player" -> sender.sendActionBar(formatted);
case "broadcast", "server" -> Bukkit.getOnlinePlayers().forEach(p -> p.sendActionBar(formatted));
case "player" -> Msg.actionBar(sender, formatted);
case "broadcast", "server" -> Bukkit.getOnlinePlayers().forEach(p -> Msg.actionBar(p, formatted));
default -> {
Player targetPlayer = Bukkit.getPlayer(target);
if (targetPlayer != null && targetPlayer.isOnline()) {
targetPlayer.sendActionBar(formatted);
Msg.actionBar(targetPlayer, formatted);
}
}
}

View file

@ -12,9 +12,7 @@ public class PlaceholderProcessor {
// Базовые плейсхолдеры
result = result.replace("{player}", player.getName());
result = result.replace("{displayname}",
net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText()
.serialize(player.displayName()));
result = result.replace("{displayname}", player.getDisplayName());
result = result.replace("{world}", player.getWorld().getName());
result = result.replace("{x}", String.valueOf(player.getLocation().getBlockX()));
result = result.replace("{y}", String.valueOf(player.getLocation().getBlockY()));

View file

@ -1,13 +1,15 @@
package com.loki.lochat.renderer;
import com.loki.lochat.channel.Channel;
import com.loki.lochat.compat.Msg;
import com.loki.lochat.renderer.components.EmojiComponent;
import com.loki.lochat.renderer.components.GradientBuilder;
import com.loki.lochat.renderer.components.UrlProcessor;
import com.loki.lochat.translate.TranslationService;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.format.TextFormatter;
import com.loki.lochat.utils.player.MentionHandler;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
@ -22,40 +24,47 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.papermc.paper.chat.ChatRenderer;
/**
* Рендерер чата — глобальный и локальный режимы
*/
public class EnhancedChatRenderer implements ChatRenderer {
public class EnhancedChatRenderer {
private static final TextColor FALLBACK_COLOR = NamedTextColor.WHITE;
private final JavaPlugin plugin;
private final boolean isGlobal;
private final Channel channel;
private final MentionHandler mentionHandler;
private final com.loki.lochat.config.ConfigManager cfg;
public EnhancedChatRenderer(JavaPlugin plugin, boolean isGlobal) {
this(plugin, isGlobal, null);
}
public EnhancedChatRenderer(JavaPlugin plugin, Channel channel) {
this(plugin, channel.isGlobalScope(), channel);
}
private EnhancedChatRenderer(JavaPlugin plugin, boolean isGlobal, Channel channel) {
this.plugin = plugin;
this.isGlobal = isGlobal;
this.channel = channel;
this.mentionHandler = new MentionHandler(plugin);
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
this.cfg = loChat.getConfigManager();
}
@Override
public Component render(Player source, Component sourceDisplayName, Component message, Audience viewer) {
public Component render(Player source, Component sourceDisplayName, Component message, Player viewer) {
// Обрабатываем сообщение
Component processed = message;
processed = TextFormatter.formatMarkdown(processed);
processed = UrlProcessor.process(processed);
processed = EmojiComponent.processEmojis(processed);
if (viewer instanceof Player vp) {
processed = mentionHandler.processMentions(processed, source, vp);
processed = applyTranslation(processed, vp);
if (viewer != null) {
processed = mentionHandler.processMentions(processed, source, viewer);
processed = applyTranslation(source, viewer, message, processed);
}
// Получаем формат из конфига
@ -68,7 +77,7 @@ public class EnhancedChatRenderer implements ChatRenderer {
Component separator = buildSeparator();
Component playerPrefix = buildPlayerPrefix(source);
Component playerSuffix = buildPlayerSuffix(source);
Component player = buildPlayerComponent(source);
Component player = buildPlayerComponent(source, sourceDisplayName);
return buildFormattedMessage(format, Map.of(
"emoji", emoji,
@ -81,6 +90,26 @@ public class EnhancedChatRenderer implements ChatRenderer {
));
}
private String emojiFor() {
if (channel != null) {
return channel.display() == null ? "" : channel.display();
}
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
return isGlobal ? appearanceCfg.getGlobalEmoji() : appearanceCfg.getLocalEmoji();
}
private String prefixText() {
if (channel != null) {
return channel.prefix() == null ? channel.id() : channel.prefix();
}
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
return isGlobal ? appearanceCfg.getGlobalText() : appearanceCfg.getLocalText();
}
private String channelColor() {
return channel != null && channel.color() != null ? channel.color() : "&7";
}
private Component buildFormattedMessage(String format, Map<String, Component> parts) {
Component result = Component.empty();
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\{([^}]+)\\}");
@ -90,7 +119,7 @@ public class EnhancedChatRenderer implements ChatRenderer {
while (matcher.find()) {
if (matcher.start() > lastEnd) {
result = result.append(Component.text(format.substring(lastEnd, matcher.start())));
result = result.append(ChatFormatter.parse(format.substring(lastEnd, matcher.start())));
}
String placeholder = matcher.group(1);
@ -100,15 +129,14 @@ public class EnhancedChatRenderer implements ChatRenderer {
}
if (lastEnd < format.length()) {
result = result.append(Component.text(format.substring(lastEnd)));
result = result.append(ChatFormatter.parse(format.substring(lastEnd)));
}
return result;
}
private Component buildEmojiComponent() {
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
String emoji = isGlobal ? appearanceCfg.getGlobalEmoji() : appearanceCfg.getLocalEmoji();
String emoji = emojiFor();
return emoji.isEmpty() ? Component.empty() : Component.text(emoji);
}
@ -123,6 +151,11 @@ public class EnhancedChatRenderer implements ChatRenderer {
}
}
String luckPermsPrefix = com.loki.lochat.utils.format.LuckPermsAffixes.prefix(player);
if (!luckPermsPrefix.isEmpty()) {
return com.loki.lochat.utils.format.ChatFormatter.parse(luckPermsPrefix);
}
return Component.empty();
}
@ -137,13 +170,20 @@ public class EnhancedChatRenderer implements ChatRenderer {
}
}
String luckPermsSuffix = com.loki.lochat.utils.format.LuckPermsAffixes.suffix(player);
if (!luckPermsSuffix.isEmpty()) {
return com.loki.lochat.utils.format.ChatFormatter.parse(luckPermsSuffix);
}
return Component.empty();
}
private Component buildPrefix() {
String text = prefixText();
if (channel != null) {
return ChatFormatter.parse(channelColor() + text);
}
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
String text = isGlobal ? appearanceCfg.getGlobalText() : appearanceCfg.getLocalText();
List<String> colors = isGlobal ? appearanceCfg.getGlobalColors() : appearanceCfg.getLocalColors();
return GradientBuilder.build(text, colors);
@ -152,12 +192,14 @@ public class EnhancedChatRenderer implements ChatRenderer {
private Component buildSeparator() {
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
String text = isGlobal ? appearanceCfg.getGlobalSeparatorText() : appearanceCfg.getLocalSeparatorText();
String color = isGlobal ? appearanceCfg.getGlobalSeparatorColor() : appearanceCfg.getLocalSeparatorColor();
String color = channel != null
? channelColor()
: (isGlobal ? appearanceCfg.getGlobalSeparatorColor() : appearanceCfg.getLocalSeparatorColor());
return Component.text(text, parseColor(color));
}
private Component buildPlayerComponent(Player player) {
Component displayName = player.displayName();
private Component buildPlayerComponent(Player player, Component sourceDisplayName) {
Component displayName = sourceDisplayName != null ? sourceDisplayName : Component.text(player.getName());
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
@ -202,31 +244,75 @@ public class EnhancedChatRenderer implements ChatRenderer {
.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
}
private Component applyTranslation(Component message, Player viewer) {
if (!cfg.isTranslationEnabled()) {
return message;
/**
* Adds the translation into the viewer's client language: as hover text over the message (default)
* or in the action bar. The translation must already be cached (see ChatEventListener#prefetchTranslation).
*/
private Component applyTranslation(Player source, Player viewer, Component original, Component processed) {
if (!cfg.isTranslationEnabled() || viewer.equals(source)) {
return processed;
}
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
TranslationService ts = loChat.getTranslationService();
if (!ts.isEnabled()) {
return message;
if (!ts.isEnabled() || source.hasPermission(cfg.getTranslationBypassPermission())) {
return processed;
}
String plainText = PlainTextComponentSerializer.plainText().serialize(message);
String plain = PlainTextComponentSerializer.plainText().serialize(original);
if (plain.isBlank()) {
return processed;
}
if ("actionbar".equalsIgnoreCase(cfg.getTranslationMode())) {
sendTranslationAsync(viewer, plain);
return processed;
}
String translated = ts.getCachedTranslation(plain, viewer.getLocale());
if (translated == null || translated.isBlank() || translated.equalsIgnoreCase(plain)) {
return processed;
}
Component hover = Component.empty();
boolean first = true;
for (String line : cfg.getTranslationHoverFormat()) {
if (!first) {
hover = hover.append(Component.newline());
}
first = false;
int idx = line.indexOf("{translation}");
if (idx < 0) {
hover = hover.append(ChatFormatter.parse(line));
} else {
hover = hover.append(ChatFormatter.parse(line.substring(0, idx)))
.append(Component.text(translated))
.append(ChatFormatter.parse(line.substring(idx + "{translation}".length())));
}
}
return processed.hoverEvent(HoverEvent.showText(hover));
}
private void sendTranslationAsync(Player viewer, String plainText) {
if (plainText.isBlank()) {
return message;
return;
}
String viewerLang = viewer.locale().getLanguage();
String translated = ts.translate(plainText, viewerLang);
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
TranslationService ts = loChat.getTranslationService();
String localeStr = viewer.getLocale();
String viewerLang = "en";
if (localeStr != null && !localeStr.isBlank()) {
viewerLang = localeStr.split("[_\\-]")[0].toLowerCase();
}
final String lang = viewerLang;
ts.translateAsync(plainText, lang)
.thenAccept(translated -> {
if (translated.equals(plainText)) {
return message;
return;
}
String langName = LANGUAGE_NAMES.getOrDefault(viewerLang, viewerLang.toUpperCase(java.util.Locale.ROOT));
Component hover = Component.text()
String langName = LANGUAGE_NAMES.getOrDefault(lang, lang.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();
return message.hoverEvent(HoverEvent.showText(hover));
Msg.actionBar(viewer, component);
});
}
private static final Map<String, String> LANGUAGE_NAMES = new HashMap<>();

View file

@ -10,65 +10,208 @@ 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;
private final String provider;
private final int timeoutMs;
public TranslationService(String endpoint, String apiKey, boolean enabled, Logger logger) {
this(endpoint, apiKey, enabled, "libretranslate", 3000, logger);
}
public TranslationService(String endpoint, String apiKey, boolean enabled, String provider, int timeoutMs,
Logger logger) {
this.endpoint = endpoint != null ? endpoint.replaceAll("/+$", "") : "";
this.apiKey = apiKey;
this.enabled = enabled;
this.provider = provider == null ? "libretranslate" : provider.toLowerCase(Locale.ROOT);
this.timeoutMs = Math.max(500, timeoutMs);
this.logger = logger;
this.httpClient = HttpClient.newBuilder()
.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;
}
});
}
private boolean isGoogle() {
return "google".equals(provider);
}
public boolean isEnabled() {
return enabled && !endpoint.isBlank();
return enabled && (isGoogle() || !endpoint.isBlank());
}
public String translate(String text, String targetLanguage) {
/** Cached translation for the given language, or null if it has not been translated yet. */
public String getCachedTranslation(String text, String language) {
return getCached(text, localeToLanguageCode(language));
}
/**
* Translates the text into every given language in parallel and waits up to timeoutMs.
* Call it off the main thread; the results end up in the cache.
*/
public void prefetch(String text, java.util.Collection<String> languages, long timeoutMs) {
if (!isEnabled() || text == null || text.isBlank()) {
return text;
return;
}
java.util.List<CompletableFuture<String>> futures = new java.util.ArrayList<>();
for (String language : languages) {
futures.add(translateAsync(text, language));
}
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.get(timeoutMs, java.util.concurrent.TimeUnit.MILLISECONDS);
} catch (Exception e) {
// timeout or failure: the message is simply sent without a translation
}
}
public CompletableFuture<String> translateAsync(String text, String targetLanguage) {
if (!isEnabled() || text == null || text.isBlank()) {
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);
String cached = getCached(text, langCode);
if (cached != null) {
return cached;
}
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 volatile long gtxBlockedUntil;
private volatile long chromeBlockedUntil;
/**
* The gtx endpoint gets IP-blocked (HTTP 429 / captcha page) very quickly, so the Chrome dictionary
* endpoint is tried first and gtx is only a fallback; a blocked endpoint is skipped for a minute.
*/
private String doFetchGoogle(String text, String targetLang) throws Exception {
long now = System.currentTimeMillis();
String q = java.net.URLEncoder.encode(text, StandardCharsets.UTF_8);
String tl = java.net.URLEncoder.encode(targetLang, StandardCharsets.UTF_8);
Exception last = null;
if (now >= chromeBlockedUntil) {
try {
return parseChromeResponse(get("https://clients5.google.com/translate_a/t?client=dict-chrome-ex&sl=auto&tl="
+ tl + "&q=" + q));
} catch (Exception e) {
chromeBlockedUntil = now + 60_000;
last = e;
}
}
if (now >= gtxBlockedUntil) {
try {
return parseGoogleResponse(get("https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&dt=t&tl="
+ tl + "&q=" + q));
} catch (Exception e) {
gtxBlockedUntil = now + 60_000;
last = e;
}
}
throw last != null ? last : new RuntimeException("all translation endpoints are on cooldown");
}
private String get(String url) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofMillis(timeoutMs))
.header("User-Agent", "Mozilla/5.0")
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Google returned " + response.statusCode());
}
return response.body();
}
/** Chrome dictionary response: [["translated","detected-language"], ...]. */
static String parseChromeResponse(String body) {
com.google.gson.JsonArray root = JsonParser.parseString(body).getAsJsonArray();
StringBuilder sb = new StringBuilder();
for (com.google.gson.JsonElement segment : root) {
if (segment.isJsonArray()) {
com.google.gson.JsonArray parts = segment.getAsJsonArray();
if (parts.size() > 0 && !parts.get(0).isJsonNull()) {
sb.append(parts.get(0).getAsString());
}
} else if (segment.isJsonPrimitive()) {
sb.append(segment.getAsString());
break;
}
}
if (sb.length() == 0) {
throw new RuntimeException("empty translation response");
}
return sb.toString();
}
/** Google response: [[["translated","original",...], ...], null, "detected-language"]. */
static String parseGoogleResponse(String body) {
com.google.gson.JsonArray root = JsonParser.parseString(body).getAsJsonArray();
StringBuilder sb = new StringBuilder();
for (com.google.gson.JsonElement segment : root.get(0).getAsJsonArray()) {
com.google.gson.JsonArray parts = segment.getAsJsonArray();
if (parts.size() > 0 && !parts.get(0).isJsonNull()) {
sb.append(parts.get(0).getAsString());
}
}
return sb.toString();
}
private String doFetch(String text, String targetLang) throws Exception {
if (isGoogle()) {
return doFetchGoogle(text, targetLang);
}
JsonObject body = new JsonObject();
body.addProperty("q", text);
body.addProperty("source", "auto");
@ -95,16 +238,37 @@ 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";
}
String lang = locale.split("_")[0].toLowerCase(Locale.ROOT);
return switch (lang) {
case "zh" -> "zh-CN";
case "zh-tw", "zh-hk" -> "zh-TW";
default -> lang;
};
String normalized = locale.toLowerCase(Locale.ROOT).replace('-', '_');
if (normalized.startsWith("zh_tw") || normalized.startsWith("zh_hk")) {
return "zh-TW";
}
String lang = normalized.split("_")[0];
return "zh".equals(lang) ? "zh-CN" : lang;
}
public void clearCache() {

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