diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..8ce7b51
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.project b/.project
new file mode 100644
index 0000000..58ebfab
--- /dev/null
+++ b/.project
@@ -0,0 +1,34 @@
+
+
+ discord-bot-bot
+ Project bot created by Buildship.
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.buildship.core.gradleprojectnature
+
+
+
+ 1783162972591
+
+ 30
+
+ org.eclipse.core.resources.regexFilterMatcher
+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+
+
+
+
diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000..f7f3856
--- /dev/null
+++ b/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,13 @@
+arguments=--init-script /home/loki/.cache/opencode/bin/jdtls/config_linux/org.eclipse.osgi/57/0/.cp/gradle/init/init.gradle
+auto.sync=false
+build.scans.enabled=false
+connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
+connection.project.dir=
+eclipse.preferences.version=1
+gradle.user.home=
+java.home=/usr/lib/jvm/java-21-openjdk
+jvm.arguments=
+offline.mode=false
+override.workspace.settings=true
+show.console.view=true
+show.executions.view=true
diff --git a/TODO.md b/TODO.md
index b060e62..4d6bb4f 100644
--- a/TODO.md
+++ b/TODO.md
@@ -251,7 +251,150 @@ feature/economy/
---
-**Версия:** 2.3 | **Последнее обновление:** 2026-07-02 | **Java 21 ✅, God Constructor ✅, Critical баги ✅, Economy ✅, Level Roles ✅, Lavalink Music ✅, SpotBugs ✅, Checkstyle ✅, CI/CD ✅**
+---
+
+## 🔴 Результаты аудита KISS/DRY/SOLID (2026-07-04)
+
+### DRY — дублирование кода (критично)
+
+| # | Что дублируется | Где | Сколько раз | Статус |
+|---|----------------|-----|-------------|--------|
+| D1 | `getDeterministicValue()` — целиком | GayCommand, FatCommand, IqCommand, PpCommand, BoykisserCommand, MustacheCommand, RateCommand | 7 | ✅ RatingService |
+| D2 | Cooldown check boilerplate (5 строк) | Все entertainment команды | 10+ | ✅ RatingService |
+| D3 | `trunc()` / `err()` helpers | MuteCommand, WarnCommand, SwapAndCaseCommand, UnmuteCommand, PunishCommand | 5 | ✅ ModerationHelper |
+| D4 | Voice connect retry loop (цикл 10×200ms) | PlayCommand, RadioManager | 2 |
+| D5 | RatingConfig — 6 методов с идентичной структурой | RatingConfig.java | 6 |
+| D6 | Social команды (hug/kiss/slap/poke/tickle/cuddle/feed/pat) — шаблон | 10 файлов | 10 |
+
+### SOLID
+
+| # | Нарушение | Файл | Описание |
+|---|-----------|------|----------|
+| S1 | SRP | BotModule.java | Биндит всё приложение |
+| S2 | SRP | SwapAndCaseCommand.java | Две разные команды в одном файле |
+| S3 | SRP | BotInitializer.java | Инициализирует 3 JDA, Lavalink, mutes, counters, health |
+| S4 | SRP | ShutdownManager.java | 9 зависимостей в конструкторе |
+| S5 | SRP | PlayCommand.java | execute() на 145 строк |
+| S6 | OCP | Meter-команды (gay/fat/iq/pp) | Добавить новую = копировать 90 строк | ✅ RatingService |
+| S7 | DIP | MuteCommand, WarnCommand | >5 конкретных репозиториев вместо интерфейсов |
+
+### Файлы >150 строк
+
+| # | Файл | Строк |
+|---|------|-------|
+| F1 | PlayCommand.java | 219 |
+| F2 | QueueSlashHandler.java | 207 |
+| F3 | MusicBotControlHandler.java | 207 |
+| F4 | QueueCommands.java | 207 |
+| F5 | TrackScheduler.java | 202 |
+| F6 | MuteCommand.java | 199 |
+| F7 | ModerationLogger.java | 199 |
+| F8 | FeatureTables.java | 193 |
+| F9 | DatabaseMigration.java | 193 |
+| F10 | VoiceChannelManager.java | 187 |
+| F11 | ServerStatsService.java | 187 |
+| F12 | VoiceSetupPrefixCommand.java | 183 |
+| F13 | RadioManager.java | 182 |
+| F14 | MetricsCollector.java | 182 |
+| F15 | WarnCommand.java | 184 |
+| F16 | BotInitializer.java | 182 |
+| F17 | SlashHandler's (Queue/Filter) | >100 |
+| F18 | GenerateRatingsCommand.java | 172 |
+| F19 | BlackjackService.java | 172 |
+| F20 | SqlEconomyRepository.java | 169 |
+| F21 | GuildMusicManager.java | 167 |
+| F22 | ProfileCommand/RankCardGen | >150 |
+
+### >3 файла в папке
+
+| # | Папка | Файлов |
+|---|-------|--------|
+| P1 | feature/entertainment/commands/ | 15 |
+| P2 | feature/social/commands/ | 10 |
+| P3 | feature/economy/command/minigame/ | 7 |
+| P4 | feature/level/ (все субпапки) | 21 |
+| P5 | feature/moderation/ (все субпакеты) | 18 |
+| P6 | feature/music/ (все субпакеты) | 22 |
+| P7 | feature/voice/ (все субпакеты) | 12 |
+
+### Нет тестов
+
+| # | Фича | Файлов |
+|---|------|--------|
+| T1 | music | 20+ |
+| T2 | voice | 12 |
+| T3 | social | 10 |
+| T4 | role | 9 |
+| T5 | radio | 5 |
+| T6 | reputation | 4 |
+| T7 | counter | 7 |
+| T8 | admin | 1 |
+| T9 | help | 2 |
+| T10 | utility | 2 |
+| T11 | welcome | 2 |
+| T12 | profile | 5 |
+| T13 | serverstats | 1 |
+
+## 📋 План рефакторинга (KISS/DRY/SOLID)
+
+### Этап 8: Убить дублирование (DRY) — в процессе
+
+- [x] D1/D2: `RatingService` с `getDeterministicValue()`, cooldown, member parsing — 7 команд переписаны
+- [x] D3: `ModerationHelper` с `trunc()`/`err()` — MuteCommand, UnmuteCommand, WarnCommand, SwapAndCaseCommand, PunishCommand
+- [ ] D4: Вынести voice connect retry в утилиту
+- [ ] D5: RatingConfig — заменить 6 методов на data-driven (Map или config)
+- [ ] D6: Social команды — единый `SocialCommand` с параметром action
+
+### Этап 9: SOLID
+
+- [ ] S1: Разбить `BotModule` на per-feature модули (FeatureModule → install в BotModule)
+- [ ] S2: Разделить `SwapAndCaseCommand` на два отдельных файла
+- [ ] S3: Выделить `MusicBotFactory`, `RadioBotFactory` из `BotInitializer`
+- [ ] S4: Уменьшить кол-во зависимостей `ShutdownManager` через события (EventBus)
+- [ ] S5: Разбить `PlayCommand.execute()` на методы
+- [ ] S6: Meter-команды сделать data-driven через `RatingService`
+- [ ] S7: Выделить интерфейсы для репозиториев/сервисов (DIP)
+
+### Этап 10: Файлы ≤150 строк
+
+- [ ] F01: PlayCommand.java — разбить на методы/классы
+- [ ] F02-04: QueueSlashHandler, MusicBotControlHandler, QueueCommands — вынести общие части
+- [ ] F05: TrackScheduler.java — вывести FilterState в отд. файл или утилиту
+- [ ] F06: MuteCommand.java — разбить на subcommand handler'ы
+- [ ] F07: ModerationLogger.java — разбить по типам событий
+- [ ] F08-09: FeatureTables, DatabaseMigration — разбить по фичам
+- [ ] F10-12: Voice* — вынести логику в сервисы
+- [ ] F13: RadioManager.java — вынести voice connect в утилиту
+- [ ] F14: MetricsCollector.java — разбить по типам метрик
+- [ ] F15: WarnCommand.java — вынести auto-mute в отдельный сервис
+- [ ] F16: BotInitializer.java — вынести фабрики ботов
+- [ ] Остальные: разбить по SOLID
+
+### Этап 11: ≤6 файлов в папке
+
+- [ ] P1: Entertainment команды — группировать по смыслу (coin/dice → `random/`, gay/iq/pp → `rating/`)
+- [ ] P2: Social команды — объединить в `SocialCommand` с параметром
+- [ ] P4: Level — разбить на commands/, service/, repository/
+- [ ] P5: Moderation — разбить по поддоменам (commands/punish, commands/utils, warn)
+- [ ] P6: Music — разбить на commands/, player/, lavalink/
+
+### Этап 12: Тесты
+
+- [ ] T1: Music — написать тесты на LavalinkManager, TrackScheduler, PlayCommand
+- [ ] T2: Voice — VoiceChannelManager, VoiceButtonHandler
+- [ ] T3: Social — интеграционные тесты с NekosLifeApi (мок)
+- [ ] T4: Radio — RadioManager, RadioCommand
+- [ ] T5: Role — CustomRoleManager, RoleCommandValidator
+- [ ] T6-13: Остальные фичи — минимум smoke test
+
+### Техдолг (баги)
+
+- [ ] B1: EmbedFactoryTest — 2 падающих теста (`withTitle`, `withColor`) — IllegalStateException
+- [ ] B2: SlowmodeCommandTest — починен (было 7 ошибок компиляции из-за JDA 6.4 SelfMember)
+
+---
+
+**Версия:** 2.3 | **Последнее обновление:** 2026-07-04 | **Java 21 ✅, God Constructor ✅, Critical баги ✅, Economy ✅, Level Roles ✅, Lavalink Music ✅, SpotBugs ✅, Checkstyle ✅, CI/CD ✅**
## ✅ Что добавлено в v2.3
diff --git a/build.gradle b/build.gradle
index 6b255bf..c600a02 100644
--- a/build.gradle
+++ b/build.gradle
@@ -23,6 +23,8 @@ dependencies {
implementation libs.guice
implementation libs.sqlite.jdbc
implementation libs.hikaricp
+ implementation libs.okhttp
+ implementation libs.logging.interceptor
testImplementation libs.junit.jupiter
testImplementation libs.mockito.core
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9d486ac..505dc61 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -10,6 +10,7 @@ caffeine = "3.2.0"
guice = "7.0.0"
sqlite = "3.49.1.0"
hikaricp = "6.3.0"
+okhttp = "5.4.0"
junit = "5.12.1"
mockito = "5.15.2"
junitPlatformLauncher = "1.12.1"
@@ -29,6 +30,8 @@ caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "c
guice = { module = "com.google.inject:guice", version.ref = "guice" }
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikaricp" }
+okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
+logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
diff --git a/src/main/java/creator/loki/core/BotInitializer.java b/src/main/java/creator/loki/core/BotInitializer.java
index 485c1aa..ed855c2 100644
--- a/src/main/java/creator/loki/core/BotInitializer.java
+++ b/src/main/java/creator/loki/core/BotInitializer.java
@@ -1,5 +1,7 @@
package creator.loki.core;
+import java.util.List;
+
import com.google.inject.Inject;
import creator.loki.config.BotConfig;
import creator.loki.core.registry.PrefixCommandRegistrar;
@@ -63,119 +65,92 @@ public class BotInitializer {
public BotCluster start() {
LoggerDecorator.startup(logger, "Инициализация main бота...");
registrar.registerAll();
- JDA mainJda = createMainJDA();
- try {
- mainJda.awaitReady();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new RuntimeException("Main бот прерван во время ожидания готовности", e);
- }
+ JDA mainJda = buildMainJDA();
mainJda.updateCommands().queue();
muteScheduler.restorePending(mainJda);
- for (var guild : mainJda.getGuilds()) {
- memberCounterListener.startScheduler(guild);
- }
+ mainJda.getGuilds().forEach(g -> memberCounterListener.startScheduler(g));
healthHttpServer.start();
- JDA musicJda = null;
- if (config.getMusicToken() != null && !config.getMusicToken().isBlank()) {
- LoggerDecorator.startup(logger, "Инициализация music бота...");
- musicJda = createMusicJDA();
- try {
- musicJda.awaitReady();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new RuntimeException("Music бот прерван во время ожидания готовности", e);
- }
- }
-
- JDA radioJda = null;
- if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) {
- LoggerDecorator.startup(logger, "Инициализация radio бота...");
- radioJda = createRadioJDA();
- try {
- radioJda.awaitReady();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new RuntimeException("Radio бот прерван во время ожидания готовности", e);
- }
+ JDA musicJda = createMusicJDA();
+ JDA radioJda = createRadioJDA();
+ if (radioJda != null) {
radioManager.setRadioJda(radioJda);
+ radioManager.setLavalinkManager(lavalinkManager);
}
-
BotCluster cluster = new BotCluster(mainJda, musicJda, radioJda);
- registerShutdownHook(cluster);
+ Runtime.getRuntime()
+ .addShutdownHook(new Thread(() -> shutdownManager.shutdown(cluster), "ShutdownHook"));
return cluster;
}
- private JDA createMainJDA() {
+ private JDA buildMainJDA() {
var builder =
- JDABuilder.createDefault(config.getMainToken())
- .setStatus(OnlineStatus.ONLINE)
- .setActivity(Activity.playing(config.getActivity()))
- .enableIntents(
- GatewayIntent.GUILD_MESSAGES,
- GatewayIntent.MESSAGE_CONTENT,
- GatewayIntent.GUILD_MEMBERS,
- GatewayIntent.GUILD_VOICE_STATES)
- .enableCache(CacheFlag.VOICE_STATE)
- .addEventListeners(listenerAggregator.getAll());
-
+ createBuilder(
+ config.getMainToken(),
+ config.getActivity(),
+ GatewayIntent.GUILD_MESSAGES,
+ GatewayIntent.MESSAGE_CONTENT,
+ GatewayIntent.GUILD_MEMBERS,
+ GatewayIntent.GUILD_VOICE_STATES);
if (lavalinkManager.getClient() != null) {
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient()));
}
-
- return builder.build();
+ builder.addEventListeners(listenerAggregator.getAll());
+ return awaitJda(builder.build());
}
private JDA createMusicJDA() {
- LavalinkManager musicLavalink =
- new LavalinkManager(config, queueRepository, config.getMusicToken());
-
+ if (config.getMusicToken() == null || config.getMusicToken().isBlank()) return null;
+ LoggerDecorator.startup(logger, "Инициализация music бота...");
+ LavalinkManager ml = new LavalinkManager(config, queueRepository, config.getMusicToken());
var builder =
- JDABuilder.createDefault(config.getMusicToken())
- .setStatus(OnlineStatus.ONLINE)
- .setActivity(Activity.playing("🎵 q!play"))
- .enableIntents(
- GatewayIntent.GUILD_MESSAGES,
- GatewayIntent.MESSAGE_CONTENT,
- GatewayIntent.GUILD_VOICE_STATES)
- .enableCache(CacheFlag.VOICE_STATE);
-
- if (musicLavalink.getClient() != null) {
- builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(musicLavalink.getClient()));
+ createBuilder(
+ config.getMusicToken(),
+ "🎵 q!play",
+ GatewayIntent.GUILD_MESSAGES,
+ GatewayIntent.MESSAGE_CONTENT,
+ GatewayIntent.GUILD_VOICE_STATES);
+ if (ml.getClient() != null) {
+ builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(ml.getClient()));
}
-
builder.addEventListeners(
new MusicBotListener(
- musicLavalink,
- queueRepository,
- new creator.loki.feature.music.core.PlayerManager(musicLavalink)));
-
- return builder.build();
+ ml, queueRepository, new creator.loki.feature.music.core.PlayerManager(ml)));
+ return awaitJda(builder.build());
}
private JDA createRadioJDA() {
+ if (config.getRadioToken() == null || config.getRadioToken().isBlank()) return null;
+ LoggerDecorator.startup(logger, "Инициализация radio бота...");
var builder =
- JDABuilder.createDefault(config.getRadioToken())
- .setStatus(OnlineStatus.ONLINE)
- .setActivity(Activity.playing("📻 r!play"))
- .enableIntents(
- GatewayIntent.GUILD_MESSAGES,
- GatewayIntent.MESSAGE_CONTENT,
- GatewayIntent.GUILD_VOICE_STATES)
- .enableCache(CacheFlag.VOICE_STATE);
-
+ createBuilder(
+ config.getRadioToken(),
+ "📻 r!play",
+ GatewayIntent.GUILD_MESSAGES,
+ GatewayIntent.MESSAGE_CONTENT,
+ GatewayIntent.GUILD_VOICE_STATES);
if (lavalinkManager.getClient() != null) {
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient()));
}
-
builder.addEventListeners(new RadioBotListener(radioManager));
-
- return builder.build();
+ return awaitJda(builder.build());
}
- private void registerShutdownHook(BotCluster cluster) {
- Runtime.getRuntime()
- .addShutdownHook(new Thread(() -> shutdownManager.shutdown(cluster), "ShutdownHook"));
+ private static JDABuilder createBuilder(String token, String activity, GatewayIntent... intents) {
+ return JDABuilder.createDefault(token)
+ .setStatus(OnlineStatus.ONLINE)
+ .setActivity(Activity.playing(activity))
+ .enableIntents(List.of(intents))
+ .enableCache(CacheFlag.VOICE_STATE);
+ }
+
+ private static JDA awaitJda(JDA jda) {
+ try {
+ jda.awaitReady();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException("JDA прерван во время ожидания готовности", e);
+ }
+ return jda;
}
}
diff --git a/src/main/java/creator/loki/feature/admin/commands/GenerateRatingsCommand.java b/src/main/java/creator/loki/feature/admin/commands/GenerateRatingsCommand.java
index 465398c..237dbe8 100644
--- a/src/main/java/creator/loki/feature/admin/commands/GenerateRatingsCommand.java
+++ b/src/main/java/creator/loki/feature/admin/commands/GenerateRatingsCommand.java
@@ -9,13 +9,10 @@ import creator.loki.config.BotConfig;
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.rating.repository.RatingsRepository;
import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.MessageEmbed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/**
- * Команда для генерации всех rating значений для всех участников сервера Доступна только
- * супер-админу
- */
public class GenerateRatingsCommand implements Command {
private static final Logger logger = LoggerFactory.getLogger(GenerateRatingsCommand.class);
private final RatingsRepository ratingsRepository;
@@ -53,120 +50,98 @@ public class GenerateRatingsCommand implements Command {
context
.getChannel()
.sendMessageEmbeds(
- EmbedFactory.create()
- .withTitle("❌ Недостаточно прав")
- .withDescription("Эта команда доступна только супер-администратору бота.")
- .withColor(Color.RED)
- .build())
+ errorEmbed(
+ "❌ Недостаточно прав", "Эта команда доступна только супер-администратору бота."))
.queue();
return;
}
-
if (context.getGuild() == null) {
context.getChannel().sendMessage("❌ Команда работает только на сервере.").queue();
return;
}
-
var initial =
context
.getChannel()
.sendMessageEmbeds(
- EmbedFactory.create()
- .withTitle("⏳ Генерация рейтингов...")
- .withDescription(
- "Начинаю генерацию всех rating значений для участников сервера...")
- .withColor(Color.YELLOW)
- .build())
+ warnEmbed(
+ "⏳ Генерация рейтингов...",
+ "Начинаю генерацию всех rating значений для участников сервера..."))
.complete();
-
try {
- String guildId = context.getGuild().getId();
- int memberCount = 0;
- int generatedCount = 0;
-
+ var guildId = context.getGuild().getId();
+ var memberCount = 0;
+ var generatedCount = 0;
for (Member member : context.getGuild().getMembers()) {
- if (member.getUser().isBot()) {
- continue;
- }
-
+ if (member.getUser().isBot()) continue;
memberCount++;
- String userId = member.getUser().getId();
-
- generateRatingForUser(guildId, userId, "ship");
- generateRatingForUser(guildId, userId, "gay");
- generateRatingForUser(guildId, userId, "iq");
- generateRatingForUser(guildId, userId, "pp");
- generateRatingForUser(guildId, userId, "mustache");
- generateRatingForUser(guildId, userId, "fat");
- generateRatingForUser(guildId, userId, "boykisser");
-
+ var userId = member.getUser().getId();
+ for (var type : new String[] {"ship", "gay", "iq", "pp", "mustache", "fat", "boykisser"}) {
+ generateRatingForUser(guildId, userId, type);
+ }
generatedCount++;
}
-
- initial
- .editMessageEmbeds(
- EmbedFactory.create()
- .withTitle("✅ Генерация завершена")
- .withDescription(
- "Успешно сгенерированы все rating значения!\n\n"
- + "**Статистика:**\n"
- + "• Обработано участников: **"
- + memberCount
- + "**\n"
- + "• Сгенерировано значений: **"
- + (generatedCount * 7)
- + "**\n"
- + "• Типы рейтингов: ship, gay, iq, pp, mustache, fat, boykisser")
- .withColor(Color.GREEN)
- .withFooter(
- "Инициатор: " + context.getAuthor().getUser().getName(),
- context.getAuthor().getAvatarUrl())
- .build())
- .queue();
-
+ initial.editMessageEmbeds(successEmbed(context, memberCount, generatedCount)).queue();
logger.info(
"Супер-админ {} сгенерировал рейтинги для {} участников",
context.getAuthor().getUser().getName(),
memberCount);
-
} catch (Exception e) {
logger.error("Ошибка при генерации рейтингов", e);
initial
.editMessageEmbeds(
- EmbedFactory.create()
- .withTitle("❌ Ошибка")
- .withDescription("Произошла ошибка при генерации рейтингов: " + e.getMessage())
- .withColor(Color.RED)
- .build())
+ errorEmbed("❌ Ошибка", "Произошла ошибка при генерации рейтингов: " + e.getMessage()))
.queue();
}
}
- /** Генерирует rating значение для пользователя */
private void generateRatingForUser(String guildId, String userId, String ratingType) {
- // Проверяем существует ли уже значение
- if (ratingsRepository.hasRating(guildId, userId, ratingType)) {
- return; // Не перезаписываем существующие значения
- }
-
- // Генерируем значение в зависимости от типа
- int value =
+ if (ratingsRepository.hasRating(guildId, userId, ratingType)) return;
+ var value =
switch (ratingType) {
- case "iq" -> random.nextInt(201); // 0-200
- case "pp" -> random.nextInt(31); // 0-30 см
+ case "iq" -> random.nextInt(201);
+ case "pp" -> random.nextInt(31);
case "mustache" -> {
- // Редкий шанс на легендарные усы
- if (random.nextInt(100) < 1) {
- yield 1000 + random.nextInt(500); // 1000-1500
- }
- yield random.nextInt(101); // 0-100
+ if (random.nextInt(100) < 1) yield 1000 + random.nextInt(500);
+ yield random.nextInt(101);
}
- case "fat" -> random.nextInt(101); // 0-100%
- case "boykisser" -> random.nextInt(101); // 0-100%
- default -> random.nextInt(101); // ship, gay: 0-100%
+ default -> random.nextInt(101);
};
-
- // Сохраняем значение в БД
ratingsRepository.setRating(guildId, userId, ratingType, value);
}
+
+ private MessageEmbed errorEmbed(String title, String description) {
+ return EmbedFactory.create()
+ .withTitle(title)
+ .withDescription(description)
+ .withColor(Color.RED)
+ .build();
+ }
+
+ private MessageEmbed warnEmbed(String title, String description) {
+ return EmbedFactory.create()
+ .withTitle(title)
+ .withDescription(description)
+ .withColor(Color.YELLOW)
+ .build();
+ }
+
+ private MessageEmbed successEmbed(CommandContext context, int memberCount, int generatedCount) {
+ return EmbedFactory.create()
+ .withTitle("✅ Генерация завершена")
+ .withDescription(
+ "Успешно сгенерированы все rating значения!\n\n"
+ + "**Статистика:**\n"
+ + "• Обработано участников: **"
+ + memberCount
+ + "**\n"
+ + "• Сгенерировано значений: **"
+ + (generatedCount * 7)
+ + "**\n"
+ + "• Типы рейтингов: ship, gay, iq, pp, mustache, fat, boykisser")
+ .withColor(Color.GREEN)
+ .withFooter(
+ "Инициатор: " + context.getAuthor().getUser().getName(),
+ context.getAuthor().getAvatarUrl())
+ .build();
+ }
}
diff --git a/src/main/java/creator/loki/feature/economy/repository/SqlEconomyRepository.java b/src/main/java/creator/loki/feature/economy/repository/SqlEconomyRepository.java
index 51eb6da..c1de989 100644
--- a/src/main/java/creator/loki/feature/economy/repository/SqlEconomyRepository.java
+++ b/src/main/java/creator/loki/feature/economy/repository/SqlEconomyRepository.java
@@ -3,6 +3,7 @@ package creator.loki.feature.economy.repository;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Types;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -19,24 +20,44 @@ public class SqlEconomyRepository implements EconomyRepository {
private static final int MAX_TOP = 100;
private final Database database;
+ private static final String COLUMNS =
+ "user_id, cash, bank, total_earned, total_spent, last_daily, daily_streak, fishing_luck, efficiency, last_fish, job, last_work";
+
public SqlEconomyRepository(Database database) {
this.database = database;
}
+ private static String selectSql() {
+ return "SELECT " + COLUMNS + " FROM economy WHERE user_id = ?";
+ }
+
+ private static String upsertSql() {
+ return "INSERT INTO economy ("
+ + COLUMNS
+ + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
+ + "ON CONFLICT(user_id) DO UPDATE SET "
+ + "cash = excluded.cash, bank = excluded.bank, total_earned = excluded.total_earned, "
+ + "total_spent = excluded.total_spent, last_daily = excluded.last_daily, "
+ + "daily_streak = excluded.daily_streak, fishing_luck = excluded.fishing_luck, "
+ + "efficiency = excluded.efficiency, last_fish = excluded.last_fish, "
+ + "job = excluded.job, last_work = excluded.last_work";
+ }
+
+ private static void setNullableString(PreparedStatement stmt, int index, String value)
+ throws SQLException {
+ if (value != null) {
+ stmt.setString(index, value);
+ } else {
+ stmt.setNull(index, Types.VARCHAR);
+ }
+ }
+
@Override
public Optional findById(String userId) {
- String sql =
- """
- SELECT user_id, cash, bank, total_earned, total_spent,
- last_daily, daily_streak, fishing_luck, efficiency,
- last_fish, job, last_work
- FROM economy
- WHERE user_id = ?
- """;
-
- try (PreparedStatement stmt = database.prepare(sql)) {
+ var sql = selectSql();
+ try (var stmt = database.prepare(sql)) {
stmt.setString(1, userId);
- try (ResultSet rs = stmt.executeQuery()) {
+ try (var rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(mapResultSetToAccount(rs));
}
@@ -49,56 +70,20 @@ public class SqlEconomyRepository implements EconomyRepository {
@Override
public void save(EconomyAccount account) {
- String sql =
- """
- INSERT INTO economy (user_id, cash, bank, total_earned, total_spent,
- last_daily, daily_streak, fishing_luck, efficiency,
- last_fish, job, last_work)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- ON CONFLICT(user_id)
- DO UPDATE SET
- cash = excluded.cash,
- bank = excluded.bank,
- total_earned = excluded.total_earned,
- total_spent = excluded.total_spent,
- last_daily = excluded.last_daily,
- daily_streak = excluded.daily_streak,
- fishing_luck = excluded.fishing_luck,
- efficiency = excluded.efficiency,
- last_fish = excluded.last_fish,
- job = excluded.job,
- last_work = excluded.last_work
- """;
-
- try (PreparedStatement stmt = database.prepare(sql)) {
+ var sql = upsertSql();
+ try (var stmt = database.prepare(sql)) {
stmt.setString(1, account.getUserId());
stmt.setLong(2, account.getCash());
stmt.setLong(3, account.getBank());
stmt.setLong(4, account.getTotalEarned());
stmt.setLong(5, account.getTotalSpent());
- if (account.getLastDaily() != null) {
- stmt.setString(6, account.getLastDaily());
- } else {
- stmt.setNull(6, java.sql.Types.VARCHAR);
- }
+ setNullableString(stmt, 6, account.getLastDaily());
stmt.setInt(7, account.getDailyStreak());
stmt.setInt(8, account.getFishingLuck());
stmt.setInt(9, account.getEfficiency());
- if (account.getLastFish() != null) {
- stmt.setString(10, account.getLastFish());
- } else {
- stmt.setNull(10, java.sql.Types.VARCHAR);
- }
- if (account.getJob() != null) {
- stmt.setString(11, account.getJob());
- } else {
- stmt.setNull(11, java.sql.Types.VARCHAR);
- }
- if (account.getLastWork() != null) {
- stmt.setString(12, account.getLastWork());
- } else {
- stmt.setNull(12, java.sql.Types.VARCHAR);
- }
+ setNullableString(stmt, 10, account.getLastFish());
+ setNullableString(stmt, 11, account.getJob());
+ setNullableString(stmt, 12, account.getLastWork());
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Error saving economy account for {}", account.getUserId(), e);
@@ -107,10 +92,10 @@ public class SqlEconomyRepository implements EconomyRepository {
@Override
public boolean exists(String userId) {
- String sql = "SELECT 1 FROM economy WHERE user_id = ?";
- try (PreparedStatement stmt = database.prepare(sql)) {
+ var sql = "SELECT 1 FROM economy WHERE user_id = ?";
+ try (var stmt = database.prepare(sql)) {
stmt.setString(1, userId);
- try (ResultSet rs = stmt.executeQuery()) {
+ try (var rs = stmt.executeQuery()) {
return rs.next();
}
} catch (SQLException e) {
@@ -121,20 +106,15 @@ public class SqlEconomyRepository implements EconomyRepository {
@Override
public List findTopByBalance(int limit) {
- int lim = clampLimit(limit);
- String sql =
- """
- SELECT user_id, cash, bank, total_earned, total_spent,
- last_daily, daily_streak, fishing_luck, efficiency,
- last_fish, job, last_work
- FROM economy
- ORDER BY (cash + bank) DESC, cash DESC, user_id ASC
- LIMIT ?
- """;
- List list = new ArrayList<>();
- try (PreparedStatement stmt = database.prepare(sql)) {
+ var lim = clampLimit(limit);
+ var sql =
+ "SELECT "
+ + COLUMNS
+ + " FROM economy ORDER BY (cash + bank) DESC, cash DESC, user_id ASC LIMIT ?";
+ var list = new ArrayList();
+ try (var stmt = database.prepare(sql)) {
stmt.setInt(1, lim);
- try (ResultSet rs = stmt.executeQuery()) {
+ try (var rs = stmt.executeQuery()) {
while (rs.next()) {
list.add(mapResultSetToAccount(rs));
}
diff --git a/src/main/java/creator/loki/feature/entertainment/EntertainmentCommandRegistrar.java b/src/main/java/creator/loki/feature/entertainment/EntertainmentCommandRegistrar.java
index 540640c..c09e7f3 100644
--- a/src/main/java/creator/loki/feature/entertainment/EntertainmentCommandRegistrar.java
+++ b/src/main/java/creator/loki/feature/entertainment/EntertainmentCommandRegistrar.java
@@ -5,41 +5,41 @@ import creator.loki.config.BotConfig;
import creator.loki.core.registry.CommandRegistrar;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.core.security.CooldownManager;
-import creator.loki.feature.entertainment.commands.BoykisserCommand;
import creator.loki.feature.entertainment.commands.CoinCommand;
import creator.loki.feature.entertainment.commands.DiceCommand;
import creator.loki.feature.entertainment.commands.EightBallCommand;
-import creator.loki.feature.entertainment.commands.FatCommand;
-import creator.loki.feature.entertainment.commands.GayCommand;
import creator.loki.feature.entertainment.commands.GlazbobaCommand;
-import creator.loki.feature.entertainment.commands.IqCommand;
import creator.loki.feature.entertainment.commands.MemeCommand;
-import creator.loki.feature.entertainment.commands.MustacheCommand;
-import creator.loki.feature.entertainment.commands.PpCommand;
import creator.loki.feature.entertainment.commands.RandomChoiceCommand;
-import creator.loki.feature.entertainment.commands.RateCommand;
+import creator.loki.feature.entertainment.commands.rating.BoykisserCommand;
+import creator.loki.feature.entertainment.commands.rating.FatCommand;
+import creator.loki.feature.entertainment.commands.rating.GayCommand;
+import creator.loki.feature.entertainment.commands.rating.IqCommand;
+import creator.loki.feature.entertainment.commands.rating.MustacheCommand;
+import creator.loki.feature.entertainment.commands.rating.PpCommand;
+import creator.loki.feature.entertainment.commands.rating.RateCommand;
+import creator.loki.feature.entertainment.service.RatingService;
import creator.loki.feature.ship.ShipCommand;
import creator.loki.feature.ship.repository.ShipPairRepository;
import creator.loki.feature.social.commands.ComplimentCommand;
-import creator.loki.feature.social.commands.CuddleCommand;
-import creator.loki.feature.social.commands.FeedCommand;
-import creator.loki.feature.social.commands.RoastCommand;
-import creator.loki.feature.social.commands.HugCommand;
-import creator.loki.feature.social.commands.KissCommand;
import creator.loki.feature.social.commands.PatCommand;
-import creator.loki.feature.social.commands.PokeCommand;
-import creator.loki.feature.social.commands.SlapCommand;
-import creator.loki.feature.social.commands.TickleCommand;
+import creator.loki.feature.social.commands.RoastCommand;
+import creator.loki.feature.social.commands.SocialCommand;
public class EntertainmentCommandRegistrar implements CommandRegistrar {
private final CooldownManager cooldownManager;
+ private final RatingService ratingService;
private final ShipPairRepository shipPairRepository;
private final BotConfig config;
@Inject
public EntertainmentCommandRegistrar(
- CooldownManager cooldownManager, ShipPairRepository shipPairRepository, BotConfig config) {
+ CooldownManager cooldownManager,
+ RatingService ratingService,
+ ShipPairRepository shipPairRepository,
+ BotConfig config) {
this.cooldownManager = cooldownManager;
+ this.ratingService = ratingService;
this.shipPairRepository = shipPairRepository;
this.config = config;
}
@@ -55,22 +55,18 @@ public class EntertainmentCommandRegistrar implements CommandRegistrar {
registry.register(new ComplimentCommand());
registry.register(new GlazbobaCommand());
- registry.register(new IqCommand(cooldownManager));
- registry.register(new GayCommand(cooldownManager));
- registry.register(new PpCommand(cooldownManager));
- registry.register(new RateCommand(cooldownManager));
- registry.register(new BoykisserCommand(cooldownManager));
- registry.register(new MustacheCommand(cooldownManager));
- registry.register(new FatCommand(cooldownManager));
+ registry.register(new IqCommand(ratingService));
+ registry.register(new GayCommand(ratingService));
+ registry.register(new PpCommand(ratingService));
+ registry.register(new RateCommand(ratingService));
+ registry.register(new BoykisserCommand(ratingService));
+ registry.register(new MustacheCommand(ratingService));
+ registry.register(new FatCommand(ratingService));
registry.register(new PatCommand(cooldownManager));
- registry.register(new HugCommand());
- registry.register(new KissCommand(cooldownManager));
- registry.register(new SlapCommand(cooldownManager));
- registry.register(new TickleCommand(cooldownManager));
- registry.register(new PokeCommand(cooldownManager));
- registry.register(new CuddleCommand(cooldownManager));
- registry.register(new FeedCommand(cooldownManager));
+ for (var flavor : SocialCommand.Flavor.values()) {
+ registry.register(new SocialCommand(flavor, cooldownManager));
+ }
registry.register(
new RandomChoiceCommand(config, "choose", RandomChoiceCommand.Flavor.DEFAULT));
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/BoykisserCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/BoykisserCommand.java
deleted file mode 100644
index 2f6a1ad..0000000
--- a/src/main/java/creator/loki/feature/entertainment/commands/BoykisserCommand.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package creator.loki.feature.entertainment.commands;
-
-import java.util.List;
-import java.util.Random;
-
-import creator.loki.command.Command;
-import creator.loki.command.CommandContext;
-import creator.loki.core.security.CooldownManager;
-import creator.loki.embed.EmbedFactory;
-import net.dv8tion.jda.api.entities.Member;
-
-/** Boykisser метр с детерминированным результатом */
-public class BoykisserCommand implements Command {
-
- private final CooldownManager cooldownManager;
-
- public BoykisserCommand(CooldownManager cooldownManager) {
- this.cooldownManager = cooldownManager;
- }
-
- @Override
- public String getName() {
- return "boykisser";
- }
-
- @Override
- public String getDescription() {
- return "На сколько ты boykisser (результат всегда одинаковый)";
- }
-
- @Override
- public String getUsage() {
- return "boykisser [@user]";
- }
-
- @Override
- public String getCategory() {
- return "Развлечения";
- }
-
- @Override
- public void execute(CommandContext context) {
- // Проверка cooldown (5 секунд)
- if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "boykisser")) {
- long remaining =
- cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "boykisser");
- context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
- return;
- }
-
- List mentioned = context.getMessage().getMentions().getMembers();
- Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
-
- if (target == null) return;
-
- if (target.getUser().isBot()) {
- context.getChannel().sendMessage("❌ Боты не целуют мальчиков!").queue();
- return;
- }
-
- // Применяем cooldown
- cooldownManager.applyCooldown(context.getAuthor().getId(), "boykisser", 5);
-
- // Детерминированный процент
- int boykisserLevel = getDeterministicValue(target.getId(), 777, 0, 100);
-
- // Прогресс-бар с поцелуями
- StringBuilder bar = new StringBuilder();
- int filled = boykisserLevel / 10;
- for (int i = 0; i < filled; i++) {
- bar.append("💋");
- }
- for (int i = filled; i < 10; i++) {
- bar.append("⬜");
- }
-
- // Определяем уровень
- RatingLevel level = RatingLevel.findLevel(boykisserLevel, RatingConfig.getBoykisserLevels());
-
- context
- .getChannel()
- .sendMessageEmbeds(
- EmbedFactory.create()
- .withTitle("💋 Boykisser Метр")
- .withDescription(target.getAsMention())
- .addField(
- "Уровень",
- bar.toString() + "\n**" + boykisserLevel + "%** - " + level.getComment(),
- false)
- .withThumbnail(target.getUser().getAvatarUrl())
- .withColor(level.getColor())
- .withFooter("💡 Детерминировано вашим Discord ID")
- .build())
- .queue();
- }
-
- private static final Random RANDOM = new Random();
-
- private int getDeterministicValue(String userId, int salt, int min, int max) {
- long seed = Long.parseLong(userId) + salt;
- RANDOM.setSeed(seed);
- return RANDOM.nextInt(max - min + 1) + min;
- }
-}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/FatCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/FatCommand.java
deleted file mode 100644
index 62d45ec..0000000
--- a/src/main/java/creator/loki/feature/entertainment/commands/FatCommand.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package creator.loki.feature.entertainment.commands;
-
-import java.util.List;
-import java.util.Random;
-
-import creator.loki.command.Command;
-import creator.loki.command.CommandContext;
-import creator.loki.core.security.CooldownManager;
-import creator.loki.embed.EmbedFactory;
-import net.dv8tion.jda.api.entities.Member;
-
-/** Fat метр - проверка на жирность */
-public class FatCommand implements Command {
-
- private final CooldownManager cooldownManager;
-
- public FatCommand(CooldownManager cooldownManager) {
- this.cooldownManager = cooldownManager;
- }
-
- @Override
- public String getName() {
- return "fat";
- }
-
- @Override
- public String getDescription() {
- return "Проверка на жирность (результат всегда одинаковый)";
- }
-
- @Override
- public String getUsage() {
- return "fat [@user]";
- }
-
- @Override
- public String getCategory() {
- return "Развлечения";
- }
-
- @Override
- public void execute(CommandContext context) {
- // Проверка cooldown (5 секунд)
- if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "fat")) {
- long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "fat");
- context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
- return;
- }
-
- List mentioned = context.getMessage().getMentions().getMembers();
- Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
-
- if (target == null) return;
-
- // Применяем cooldown
- cooldownManager.applyCooldown(context.getAuthor().getId(), "fat", 5);
-
- // Детерминированный процент от 0 до 100
- int fatLevel = getDeterministicValue(target.getId(), 999, 0, 100);
-
- // Прогресс-бар
- StringBuilder bar = new StringBuilder();
- int filled = fatLevel / 10;
- for (int i = 0; i < filled; i++) {
- bar.append("🟩");
- }
- for (int i = filled; i < 10; i++) {
- bar.append("⬜");
- }
-
- // Определяем уровень
- RatingLevel level = RatingLevel.findLevel(fatLevel, RatingConfig.getFatLevels());
-
- context
- .getChannel()
- .sendMessageEmbeds(
- EmbedFactory.create()
- .withTitle("🐷 Fat Метр")
- .withDescription(target.getAsMention())
- .addField(
- "Жирность",
- bar.toString() + "\n**" + fatLevel + "%** - " + level.getComment(),
- false)
- .withThumbnail(target.getUser().getAvatarUrl())
- .withColor(level.getColor())
- .withFooter("💡 Детерминировано вашим Discord ID")
- .build())
- .queue();
- }
-
- private static final Random RANDOM = new Random();
-
- private int getDeterministicValue(String userId, int salt, int min, int max) {
- long seed = Long.parseLong(userId) + salt;
- RANDOM.setSeed(seed);
- return RANDOM.nextInt(max - min + 1) + min;
- }
-}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/GayCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/GayCommand.java
deleted file mode 100644
index 14f253d..0000000
--- a/src/main/java/creator/loki/feature/entertainment/commands/GayCommand.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package creator.loki.feature.entertainment.commands;
-
-import java.awt.Color;
-import java.util.List;
-import java.util.Random;
-
-import creator.loki.command.Command;
-import creator.loki.command.CommandContext;
-import creator.loki.core.security.CooldownManager;
-import creator.loki.embed.EmbedFactory;
-import net.dv8tion.jda.api.entities.Member;
-
-/** Gay-метр с детерминированным результатом */
-public class GayCommand implements Command {
-
- private final CooldownManager cooldownManager;
-
- public GayCommand(CooldownManager cooldownManager) {
- this.cooldownManager = cooldownManager;
- }
-
- @Override
- public String getName() {
- return "gay";
- }
-
- @Override
- public String getDescription() {
- return "Гей-метр (результат всегда одинаковый)";
- }
-
- @Override
- public String getUsage() {
- return "gay [@user]";
- }
-
- @Override
- public String getCategory() {
- return "Развлечения";
- }
-
- @Override
- public void execute(CommandContext context) {
- // Проверка cooldown (5 секунд)
- if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "gay")) {
- long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "gay");
- context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
- return;
- }
-
- List mentioned = context.getMessage().getMentions().getMembers();
- Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
-
- if (target == null) return;
-
- if (target.getUser().isBot()) {
- context.getChannel().sendMessage("❌ Боты не имеют ориентации!").queue();
- return;
- }
-
- // Применяем cooldown
- cooldownManager.applyCooldown(context.getAuthor().getId(), "gay", 5);
-
- // Детерминированный процент
- int gayLevel = getDeterministicValue(target.getId(), 456, 0, 100);
-
- // Прогресс-бар с радугой
- String[] rainbow = {"🟥", "🟧", "🟨", "🟩", "🟦", "🟪"};
- StringBuilder bar = new StringBuilder();
- int filled = Math.min(gayLevel / 17 + 1, 6);
- for (int i = 0; i < filled; i++) {
- bar.append(rainbow[i]);
- }
- for (int i = filled; i < 6; i++) {
- bar.append("⬜");
- }
-
- context
- .getChannel()
- .sendMessageEmbeds(
- EmbedFactory.create()
- .withTitle("🏳️🌈 Гей-метр")
- .withDescription(target.getAsMention())
- .addField("Уровень", bar.toString() + "\n**" + gayLevel + "%**", false)
- .withThumbnail(target.getUser().getAvatarUrl())
- .withColor(new Color(255, 0, 255))
- .withFooter("💡 Это просто мем, не воспринимайте серьезно")
- .build())
- .queue();
- }
-
- private static final Random RANDOM = new Random();
-
- private int getDeterministicValue(String userId, int salt, int min, int max) {
- long seed = Long.parseLong(userId) + salt;
- RANDOM.setSeed(seed);
- return RANDOM.nextInt(max - min + 1) + min;
- }
-}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/IqCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/IqCommand.java
deleted file mode 100644
index 5f83593..0000000
--- a/src/main/java/creator/loki/feature/entertainment/commands/IqCommand.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package creator.loki.feature.entertainment.commands;
-
-import java.util.List;
-import java.util.Random;
-
-import creator.loki.command.Command;
-import creator.loki.command.CommandContext;
-import creator.loki.core.security.CooldownManager;
-import creator.loki.embed.EmbedFactory;
-import net.dv8tion.jda.api.entities.Member;
-
-/** IQ тест с детерминированным результатом */
-public class IqCommand implements Command {
-
- private final CooldownManager cooldownManager;
-
- public IqCommand(CooldownManager cooldownManager) {
- this.cooldownManager = cooldownManager;
- }
-
- @Override
- public String getName() {
- return "iq";
- }
-
- @Override
- public String getDescription() {
- return "Тест IQ (результат всегда одинаковый)";
- }
-
- @Override
- public String getUsage() {
- return "iq [@user]";
- }
-
- @Override
- public String getCategory() {
- return "Развлечения";
- }
-
- @Override
- public void execute(CommandContext context) {
- // Проверка cooldown (5 секунд)
- if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "iq")) {
- long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "iq");
- context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
- return;
- }
-
- List mentioned = context.getMessage().getMentions().getMembers();
- Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
-
- if (target == null) return;
-
- // Применяем cooldown
- cooldownManager.applyCooldown(context.getAuthor().getId(), "iq", 5);
-
- int iq;
- if (target.getUser().isBot()) {
- iq = 9000; // Боты умные!
- } else if (target.getId().equals("1414975197269987471")) {
- iq = 1000; // Специальное значение
- } else {
- // Детерминированный IQ от 0 до 140
- iq = getDeterministicValue(target.getId(), 123, 0, 140);
- }
-
- // Определяем уровень
- RatingLevel level = RatingLevel.findLevel(iq, RatingConfig.getIqLevels());
-
- context
- .getChannel()
- .sendMessageEmbeds(
- EmbedFactory.create()
- .withTitle("🧠 Тест IQ")
- .withDescription("Результат для " + target.getAsMention())
- .addField("IQ Уровень", "**" + iq + "** - " + level.getComment(), false)
- .withThumbnail(target.getUser().getAvatarUrl())
- .withColor(level.getColor())
- .withFooter("💡 Научно доказано вашим ID")
- .build())
- .queue();
- }
-
- private static final Random RANDOM = new Random();
-
- private int getDeterministicValue(String userId, int salt, int min, int max) {
- long seed = Long.parseLong(userId) + salt;
- RANDOM.setSeed(seed);
- return RANDOM.nextInt(max - min + 1) + min;
- }
-}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/MustacheCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/MustacheCommand.java
deleted file mode 100644
index 6090793..0000000
--- a/src/main/java/creator/loki/feature/entertainment/commands/MustacheCommand.java
+++ /dev/null
@@ -1,107 +0,0 @@
-package creator.loki.feature.entertainment.commands;
-
-import java.util.List;
-import java.util.Random;
-
-import creator.loki.command.Command;
-import creator.loki.command.CommandContext;
-import creator.loki.core.security.CooldownManager;
-import creator.loki.embed.EmbedFactory;
-import net.dv8tion.jda.api.entities.Member;
-
-/** Mustache метр - проверка на усатость */
-public class MustacheCommand implements Command {
-
- private final CooldownManager cooldownManager;
-
- public MustacheCommand(CooldownManager cooldownManager) {
- this.cooldownManager = cooldownManager;
- }
-
- @Override
- public String getName() {
- return "mustache";
- }
-
- @Override
- public String getDescription() {
- return "Проверка на усатость (результат всегда одинаковый)";
- }
-
- @Override
- public String getUsage() {
- return "mustache [@user]";
- }
-
- @Override
- public String getCategory() {
- return "Развлечения";
- }
-
- @Override
- public void execute(CommandContext context) {
- // Проверка cooldown (5 секунд)
- if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "mustache")) {
- long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "mustache");
- context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
- return;
- }
-
- List mentioned = context.getMessage().getMentions().getMembers();
- Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
-
- if (target == null) return;
-
- // Применяем cooldown
- cooldownManager.applyCooldown(context.getAuthor().getId(), "mustache", 5);
-
- int mustacheLevel;
-
- // Специальные значения
- if (target.getUser().isBot() || target.getId().equals("1414975197269987471")) {
- mustacheLevel = 0; // Боты и специальный ID = 0%
- } else if (target.getId().equals("678283217094180915")) {
- mustacheLevel = 1000; // Специальный ID = 1000%
- } else {
- // Детерминированный процент от 0 до 100
- mustacheLevel = getDeterministicValue(target.getId(), 888, 0, 100);
- }
-
- // Прогресс-бар с усами
- StringBuilder bar = new StringBuilder();
- int filled = Math.min(mustacheLevel / 10, 10);
- for (int i = 0; i < filled; i++) {
- bar.append("🧔");
- }
- for (int i = filled; i < 10; i++) {
- bar.append("⬜");
- }
-
- // Определяем уровень
- RatingLevel level = RatingLevel.findLevel(mustacheLevel, RatingConfig.getMustacheLevels());
-
- context
- .getChannel()
- .sendMessageEmbeds(
- EmbedFactory.create()
- .withTitle("🧔 Mustache Метр")
- .withDescription(target.getAsMention())
- .addField(
- "Усатость",
- bar.toString() + "\n**" + mustacheLevel + "%** - " + level.getComment(),
- false)
- .withThumbnail(target.getUser().getAvatarUrl())
- .withColor(level.getColor())
- .withFooter("💡 Детерминировано вашим Discord ID")
- .build())
- .queue();
- }
-
- private static final Random RANDOM = new Random();
-
- private int getDeterministicValue(String userId, int salt, int min, int max) {
- long seed = Long.parseLong(userId) + salt;
- RANDOM.setSeed(seed);
- return RANDOM.nextInt(max - min + 1) + min;
- }
-}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/PpCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/PpCommand.java
deleted file mode 100644
index a55accd..0000000
--- a/src/main/java/creator/loki/feature/entertainment/commands/PpCommand.java
+++ /dev/null
@@ -1,111 +0,0 @@
-package creator.loki.feature.entertainment.commands;
-
-import java.util.List;
-import java.util.Random;
-
-import creator.loki.command.Command;
-import creator.loki.command.CommandContext;
-import creator.loki.core.security.CooldownManager;
-import creator.loki.embed.EmbedFactory;
-import net.dv8tion.jda.api.entities.Member;
-
-/** PP размер с детерминированным результатом */
-public class PpCommand implements Command {
-
- private final CooldownManager cooldownManager;
-
- public PpCommand(CooldownManager cooldownManager) {
- this.cooldownManager = cooldownManager;
- }
-
- @Override
- public String getName() {
- return "pp";
- }
-
- @Override
- public String getDescription() {
- return "Измеритель pp (результат всегда одинаковый)";
- }
-
- @Override
- public String getUsage() {
- return "pp [@user]";
- }
-
- @Override
- public String getCategory() {
- return "Развлечения";
- }
-
- @Override
- public void execute(CommandContext context) {
- // Проверка cooldown (5 секунд)
- if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "pp")) {
- long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "pp");
- context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
- return;
- }
-
- List mentioned = context.getMessage().getMentions().getMembers();
- Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
-
- if (target == null) return;
-
- if (target.getUser().isBot()) {
- context.getChannel().sendMessage("❌ У ботов нет pp!").queue();
- return;
- }
-
- // Применяем cooldown
- cooldownManager.applyCooldown(context.getAuthor().getId(), "pp", 5);
-
- // Специальное значение для конкретного пользователя
- int size;
- if (target.getId().equals("1414975197269987471")) {
- size = 100;
- } else {
- // Детерминированный размер от 0 до 30 см
- size = getDeterministicValue(target.getId(), 6969, 0, 30);
- }
-
- // Визуализация
- StringBuilder ppVisual = new StringBuilder("8");
- for (int i = 0; i < size; i++) {
- ppVisual.append("=");
- }
- ppVisual.append("D");
-
- // Определяем уровень
- RatingLevel level = RatingLevel.findLevel(size, RatingConfig.getPpLevels());
-
- context
- .getChannel()
- .sendMessageEmbeds(
- EmbedFactory.create()
- .withTitle("🍆 PP Размер")
- .withDescription(target.getAsMention())
- .addField(
- "Размер",
- "```"
- + ppVisual.toString()
- + "```\n**"
- + size
- + " см** - "
- + level.getComment(),
- false)
- .withThumbnail(target.getUser().getAvatarUrl())
- .withColor(level.getColor())
- .withFooter("💡 Детерминировано вашим Discord ID")
- .build())
- .queue();
- }
-
- private static final Random RANDOM = new Random();
-
- private int getDeterministicValue(String userId, int salt, int min, int max) {
- long seed = Long.parseLong(userId) + salt;
- RANDOM.setSeed(seed);
- return RANDOM.nextInt(max - min + 1) + min;
- }
-}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/RatingConfig.java b/src/main/java/creator/loki/feature/entertainment/commands/RatingConfig.java
deleted file mode 100644
index 26cd9f6..0000000
--- a/src/main/java/creator/loki/feature/entertainment/commands/RatingConfig.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package creator.loki.feature.entertainment.commands;
-
-import java.awt.Color;
-import java.util.Arrays;
-import java.util.List;
-
-/** Конфигурации уровней для различных rating команд */
-public class RatingConfig {
-
- /** Уровни для Mustache команды */
- public static List getMustacheLevels() {
- return Arrays.asList(
- new RatingLevel(1000, "🎩 ЛЕГЕНДАРНЫЕ УСИЩИ!", new Color(255, 215, 0)),
- new RatingLevel(80, "🧔 Роскошные усы!", new Color(139, 69, 19)),
- new RatingLevel(60, "👨 Хорошие усы", new Color(160, 82, 45)),
- new RatingLevel(40, "😊 Усики растут", new Color(205, 133, 63)),
- new RatingLevel(20, "🙂 Лёгкая щетина", new Color(210, 180, 140)),
- new RatingLevel(1, "😐 Почти нет усов", new Color(128, 128, 128)),
- new RatingLevel(0, "🪒 Гладко выбрит", new Color(200, 200, 200)));
- }
-
- /** Уровни для IQ команды */
- public static List getIqLevels() {
- return Arrays.asList(
- new RatingLevel(140, "🧠 Гений!", new Color(255, 215, 0)),
- new RatingLevel(120, "🎓 Очень умный", new Color(100, 255, 150)),
- new RatingLevel(100, "📚 Средний", new Color(52, 152, 219)),
- new RatingLevel(80, "📖 Ниже среднего", new Color(255, 165, 0)),
- new RatingLevel(0, "🤔 Низкий", new Color(231, 76, 60)));
- }
-
- /** Уровни для Fat команды */
- public static List getFatLevels() {
- return Arrays.asList(
- new RatingLevel(90, "🐷 СВИНОТА!", new Color(255, 0, 0)),
- new RatingLevel(70, "🍔 Очень жирный", new Color(255, 100, 0)),
- new RatingLevel(50, "🍕 Жирноват", new Color(255, 165, 0)),
- new RatingLevel(30, "👍 Нормальный вес", new Color(0, 200, 0)),
- new RatingLevel(10, "🥗 Худой", new Color(100, 200, 255)),
- new RatingLevel(0, "💀 Очень худой", new Color(150, 150, 150)));
- }
-
- /** Уровни для PP команды (размер в см) */
- public static List getPpLevels() {
- return Arrays.asList(
- new RatingLevel(25, "😳 НЕВЕРОЯТНО!", new Color(255, 215, 0)),
- new RatingLevel(20, "😮 Впечатляет", new Color(100, 255, 150)),
- new RatingLevel(15, "👍 Выше среднего", new Color(52, 152, 219)),
- new RatingLevel(10, "🙂 Средний", new Color(128, 128, 128)),
- new RatingLevel(5, "😐 Ниже среднего", new Color(255, 165, 0)),
- new RatingLevel(0, "💀 Микро", new Color(231, 76, 60)));
- }
-
- /** Уровни для Boykisser команды */
- public static List getBoykisserLevels() {
- return Arrays.asList(
- new RatingLevel(90, "💋 ЛЕГЕНДАРНЫЙ BOYKISSER!", new Color(255, 105, 180)),
- new RatingLevel(70, "😘 Активный boykisser", new Color(255, 182, 193)),
- new RatingLevel(50, "😊 Любит целоваться", new Color(255, 192, 203)),
- new RatingLevel(30, "🙂 Иногда целуется", new Color(255, 218, 185)),
- new RatingLevel(10, "😐 Редко целуется", new Color(176, 196, 222)),
- new RatingLevel(0, "😶 Не целуется", new Color(128, 128, 128)));
- }
-}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/BoykisserCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/BoykisserCommand.java
new file mode 100644
index 0000000..1c15407
--- /dev/null
+++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/BoykisserCommand.java
@@ -0,0 +1,70 @@
+package creator.loki.feature.entertainment.commands.rating;
+
+import com.google.inject.Inject;
+import creator.loki.command.Command;
+import creator.loki.command.CommandContext;
+import creator.loki.embed.EmbedFactory;
+import creator.loki.feature.entertainment.service.RatingService;
+import net.dv8tion.jda.api.entities.Member;
+
+public class BoykisserCommand implements Command {
+
+ private final RatingService ratingService;
+
+ @Inject
+ public BoykisserCommand(RatingService ratingService) {
+ this.ratingService = ratingService;
+ }
+
+ @Override
+ public String getName() {
+ return "boykisser";
+ }
+
+ @Override
+ public String getDescription() {
+ return "На сколько ты boykisser (результат всегда одинаковый)";
+ }
+
+ @Override
+ public String getUsage() {
+ return "boykisser [@user]";
+ }
+
+ @Override
+ public String getCategory() {
+ return "Развлечения";
+ }
+
+ @Override
+ public void execute(CommandContext context) {
+ if (!ratingService.checkAndApplyCooldown(context, "boykisser")) return;
+ Member target = ratingService.resolveTarget(context);
+ if (target == null) return;
+ if (target.getUser().isBot()) {
+ context.getChannel().sendMessage("❌ Боты не целуют мальчиков!").queue();
+ return;
+ }
+
+ int boykisserLevel = ratingService.getDeterministicValue(target.getId(), 777, 0, 100);
+ StringBuilder bar = new StringBuilder();
+ int filled = boykisserLevel / 10;
+ for (int i = 0; i < filled; i++) bar.append("💋");
+ for (int i = filled; i < 10; i++) bar.append("⬜");
+
+ RatingLevel level = RatingLevel.findLevel(boykisserLevel, RatingConfig.getBoykisserLevels());
+ context
+ .getChannel()
+ .sendMessageEmbeds(
+ EmbedFactory.create()
+ .withTitle("💋 Boykisser Метр")
+ .withDescription(target.getAsMention())
+ .addField(
+ "Уровень", bar + "\n**" + boykisserLevel + "%** - " + level.getComment(), false)
+ .withThumbnail(target.getUser().getAvatarUrl())
+ .withColor(level.getColor())
+ .withFooter("💡 Детерминировано вашим Discord ID")
+ .build())
+ .queue();
+ }
+}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/FatCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/FatCommand.java
new file mode 100644
index 0000000..3efd589
--- /dev/null
+++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/FatCommand.java
@@ -0,0 +1,66 @@
+package creator.loki.feature.entertainment.commands.rating;
+
+import com.google.inject.Inject;
+import creator.loki.command.Command;
+import creator.loki.command.CommandContext;
+import creator.loki.embed.EmbedFactory;
+import creator.loki.feature.entertainment.service.RatingService;
+import net.dv8tion.jda.api.entities.Member;
+
+public class FatCommand implements Command {
+
+ private final RatingService ratingService;
+
+ @Inject
+ public FatCommand(RatingService ratingService) {
+ this.ratingService = ratingService;
+ }
+
+ @Override
+ public String getName() {
+ return "fat";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Проверка на жирность (результат всегда одинаковый)";
+ }
+
+ @Override
+ public String getUsage() {
+ return "fat [@user]";
+ }
+
+ @Override
+ public String getCategory() {
+ return "Развлечения";
+ }
+
+ @Override
+ public void execute(CommandContext context) {
+ if (!ratingService.checkAndApplyCooldown(context, "fat")) return;
+ Member target = ratingService.resolveTarget(context);
+ if (target == null) return;
+
+ int fatLevel = ratingService.getDeterministicValue(target.getId(), 999, 0, 100);
+ StringBuilder bar = new StringBuilder();
+ int filled = fatLevel / 10;
+ for (int i = 0; i < filled; i++) bar.append("🟩");
+ for (int i = filled; i < 10; i++) bar.append("⬜");
+
+ RatingLevel level = RatingLevel.findLevel(fatLevel, RatingConfig.getFatLevels());
+ context
+ .getChannel()
+ .sendMessageEmbeds(
+ EmbedFactory.create()
+ .withTitle("🐷 Fat Метр")
+ .withDescription(target.getAsMention())
+ .addField(
+ "Жирность", bar + "\n**" + fatLevel + "%** - " + level.getComment(), false)
+ .withThumbnail(target.getUser().getAvatarUrl())
+ .withColor(level.getColor())
+ .withFooter("💡 Детерминировано вашим Discord ID")
+ .build())
+ .queue();
+ }
+}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/GayCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/GayCommand.java
new file mode 100644
index 0000000..5b8a3f4
--- /dev/null
+++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/GayCommand.java
@@ -0,0 +1,71 @@
+package creator.loki.feature.entertainment.commands.rating;
+
+import java.awt.Color;
+
+import com.google.inject.Inject;
+import creator.loki.command.Command;
+import creator.loki.command.CommandContext;
+import creator.loki.embed.EmbedFactory;
+import creator.loki.feature.entertainment.service.RatingService;
+import net.dv8tion.jda.api.entities.Member;
+
+public class GayCommand implements Command {
+
+ private final RatingService ratingService;
+
+ @Inject
+ public GayCommand(RatingService ratingService) {
+ this.ratingService = ratingService;
+ }
+
+ @Override
+ public String getName() {
+ return "gay";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Гей-метр (результат всегда одинаковый)";
+ }
+
+ @Override
+ public String getUsage() {
+ return "gay [@user]";
+ }
+
+ @Override
+ public String getCategory() {
+ return "Развлечения";
+ }
+
+ @Override
+ public void execute(CommandContext context) {
+ if (!ratingService.checkAndApplyCooldown(context, "gay")) return;
+ Member target = ratingService.resolveTarget(context);
+ if (target == null) return;
+ if (target.getUser().isBot()) {
+ context.getChannel().sendMessage("❌ Боты не имеют ориентации!").queue();
+ return;
+ }
+
+ int gayLevel = ratingService.getDeterministicValue(target.getId(), 456, 0, 100);
+ String[] rainbow = {"🟥", "🟧", "🟨", "🟩", "🟦", "🟪"};
+ StringBuilder bar = new StringBuilder();
+ int filled = Math.min(gayLevel / 17 + 1, 6);
+ for (int i = 0; i < filled; i++) bar.append(rainbow[i]);
+ for (int i = filled; i < 6; i++) bar.append("⬜");
+
+ context
+ .getChannel()
+ .sendMessageEmbeds(
+ EmbedFactory.create()
+ .withTitle("🏳️🌈 Гей-метр")
+ .withDescription(target.getAsMention())
+ .addField("Уровень", bar + "\n**" + gayLevel + "%**", false)
+ .withThumbnail(target.getUser().getAvatarUrl())
+ .withColor(new Color(255, 0, 255))
+ .withFooter("💡 Это просто мем, не воспринимайте серьезно")
+ .build())
+ .queue();
+ }
+}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/IqCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/IqCommand.java
new file mode 100644
index 0000000..5f0723d
--- /dev/null
+++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/IqCommand.java
@@ -0,0 +1,68 @@
+package creator.loki.feature.entertainment.commands.rating;
+
+import com.google.inject.Inject;
+import creator.loki.command.Command;
+import creator.loki.command.CommandContext;
+import creator.loki.embed.EmbedFactory;
+import creator.loki.feature.entertainment.service.RatingService;
+import net.dv8tion.jda.api.entities.Member;
+
+public class IqCommand implements Command {
+
+ private final RatingService ratingService;
+
+ @Inject
+ public IqCommand(RatingService ratingService) {
+ this.ratingService = ratingService;
+ }
+
+ @Override
+ public String getName() {
+ return "iq";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Тест IQ (результат всегда одинаковый)";
+ }
+
+ @Override
+ public String getUsage() {
+ return "iq [@user]";
+ }
+
+ @Override
+ public String getCategory() {
+ return "Развлечения";
+ }
+
+ @Override
+ public void execute(CommandContext context) {
+ if (!ratingService.checkAndApplyCooldown(context, "iq")) return;
+ Member target = ratingService.resolveTarget(context);
+ if (target == null) return;
+
+ int iq;
+ if (target.getUser().isBot()) {
+ iq = 9000;
+ } else if (target.getId().equals("1414975197269987471")) {
+ iq = 1000;
+ } else {
+ iq = ratingService.getDeterministicValue(target.getId(), 123, 0, 140);
+ }
+
+ RatingLevel level = RatingLevel.findLevel(iq, RatingConfig.getIqLevels());
+ context
+ .getChannel()
+ .sendMessageEmbeds(
+ EmbedFactory.create()
+ .withTitle("🧠 Тест IQ")
+ .withDescription("Результат для " + target.getAsMention())
+ .addField("IQ Уровень", "**" + iq + "** - " + level.getComment(), false)
+ .withThumbnail(target.getUser().getAvatarUrl())
+ .withColor(level.getColor())
+ .withFooter("💡 Научно доказано вашим ID")
+ .build())
+ .queue();
+ }
+}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/MustacheCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/MustacheCommand.java
new file mode 100644
index 0000000..c924d99
--- /dev/null
+++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/MustacheCommand.java
@@ -0,0 +1,74 @@
+package creator.loki.feature.entertainment.commands.rating;
+
+import com.google.inject.Inject;
+import creator.loki.command.Command;
+import creator.loki.command.CommandContext;
+import creator.loki.embed.EmbedFactory;
+import creator.loki.feature.entertainment.service.RatingService;
+import net.dv8tion.jda.api.entities.Member;
+
+public class MustacheCommand implements Command {
+
+ private final RatingService ratingService;
+
+ @Inject
+ public MustacheCommand(RatingService ratingService) {
+ this.ratingService = ratingService;
+ }
+
+ @Override
+ public String getName() {
+ return "mustache";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Проверка на усатость (результат всегда одинаковый)";
+ }
+
+ @Override
+ public String getUsage() {
+ return "mustache [@user]";
+ }
+
+ @Override
+ public String getCategory() {
+ return "Развлечения";
+ }
+
+ @Override
+ public void execute(CommandContext context) {
+ if (!ratingService.checkAndApplyCooldown(context, "mustache")) return;
+ Member target = ratingService.resolveTarget(context);
+ if (target == null) return;
+
+ int mustacheLevel;
+ if (target.getUser().isBot() || target.getId().equals("1414975197269987471")) {
+ mustacheLevel = 0;
+ } else if (target.getId().equals("678283217094180915")) {
+ mustacheLevel = 1000;
+ } else {
+ mustacheLevel = ratingService.getDeterministicValue(target.getId(), 888, 0, 100);
+ }
+
+ StringBuilder bar = new StringBuilder();
+ int filled = Math.min(mustacheLevel / 10, 10);
+ for (int i = 0; i < filled; i++) bar.append("🧔");
+ for (int i = filled; i < 10; i++) bar.append("⬜");
+
+ RatingLevel level = RatingLevel.findLevel(mustacheLevel, RatingConfig.getMustacheLevels());
+ context
+ .getChannel()
+ .sendMessageEmbeds(
+ EmbedFactory.create()
+ .withTitle("🧔 Mustache Метр")
+ .withDescription(target.getAsMention())
+ .addField(
+ "Усатость", bar + "\n**" + mustacheLevel + "%** - " + level.getComment(), false)
+ .withThumbnail(target.getUser().getAvatarUrl())
+ .withColor(level.getColor())
+ .withFooter("💡 Детерминировано вашим Discord ID")
+ .build())
+ .queue();
+ }
+}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/PpCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/PpCommand.java
new file mode 100644
index 0000000..35f0905
--- /dev/null
+++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/PpCommand.java
@@ -0,0 +1,77 @@
+package creator.loki.feature.entertainment.commands.rating;
+
+import com.google.inject.Inject;
+import creator.loki.command.Command;
+import creator.loki.command.CommandContext;
+import creator.loki.embed.EmbedFactory;
+import creator.loki.feature.entertainment.service.RatingService;
+import net.dv8tion.jda.api.entities.Member;
+
+public class PpCommand implements Command {
+
+ private final RatingService ratingService;
+
+ @Inject
+ public PpCommand(RatingService ratingService) {
+ this.ratingService = ratingService;
+ }
+
+ @Override
+ public String getName() {
+ return "pp";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Измеритель pp (результат всегда одинаковый)";
+ }
+
+ @Override
+ public String getUsage() {
+ return "pp [@user]";
+ }
+
+ @Override
+ public String getCategory() {
+ return "Развлечения";
+ }
+
+ @Override
+ public void execute(CommandContext context) {
+ if (!ratingService.checkAndApplyCooldown(context, "pp")) return;
+ Member target = ratingService.resolveTarget(context);
+ if (target == null) return;
+ if (target.getUser().isBot()) {
+ context.getChannel().sendMessage("❌ У ботов нет pp!").queue();
+ return;
+ }
+
+ int size;
+ if (target.getId().equals("1414975197269987471")) {
+ size = 100;
+ } else {
+ size = ratingService.getDeterministicValue(target.getId(), 6969, 0, 30);
+ }
+
+ StringBuilder ppVisual = new StringBuilder("8");
+ for (int i = 0; i < size; i++) ppVisual.append("=");
+ ppVisual.append("D");
+
+ RatingLevel level = RatingLevel.findLevel(size, RatingConfig.getPpLevels());
+ context
+ .getChannel()
+ .sendMessageEmbeds(
+ EmbedFactory.create()
+ .withTitle("🍆 PP Размер")
+ .withDescription(target.getAsMention())
+ .addField(
+ "Размер",
+ "```" + ppVisual + "```\n**" + size + " см** - " + level.getComment(),
+ false)
+ .withThumbnail(target.getUser().getAvatarUrl())
+ .withColor(level.getColor())
+ .withFooter("💡 Детерминировано вашим Discord ID")
+ .build())
+ .queue();
+ }
+}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/RateCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/RateCommand.java
similarity index 56%
rename from src/main/java/creator/loki/feature/entertainment/commands/RateCommand.java
rename to src/main/java/creator/loki/feature/entertainment/commands/rating/RateCommand.java
index c48397c..1b9ee50 100644
--- a/src/main/java/creator/loki/feature/entertainment/commands/RateCommand.java
+++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/RateCommand.java
@@ -1,24 +1,23 @@
-package creator.loki.feature.entertainment.commands;
+package creator.loki.feature.entertainment.commands.rating;
import java.awt.Color;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
-import java.util.Random;
+import com.google.inject.Inject;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
-import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
+import creator.loki.feature.entertainment.service.RatingService;
import net.dv8tion.jda.api.entities.Member;
-/** Оценка пользователя из 10 с детерминированным результатом */
public class RateCommand implements Command {
- private final CooldownManager cooldownManager;
+ private final RatingService ratingService;
- public RateCommand(CooldownManager cooldownManager) {
- this.cooldownManager = cooldownManager;
+ @Inject
+ public RateCommand(RatingService ratingService) {
+ this.ratingService = ratingService;
}
@Override
@@ -43,28 +42,13 @@ public class RateCommand implements Command {
@Override
public void execute(CommandContext context) {
- // Проверка cooldown (5 секунд)
- if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "rate")) {
- long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "rate");
- context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
- return;
- }
-
- List mentioned = context.getMessage().getMentions().getMembers();
- Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
-
+ if (!ratingService.checkAndApplyCooldown(context, "rate")) return;
+ Member target = ratingService.resolveTarget(context);
if (target == null) return;
- // Применяем cooldown
- cooldownManager.applyCooldown(context.getAuthor().getId(), "rate", 5);
-
- // Детерминированная оценка от 1 до 10
- int rating = getDeterministicValue(target.getId(), 420, 1, 10);
-
- // Визуализация звездами
+ int rating = ratingService.getDeterministicValue(target.getId(), 420, 1, 10);
String stars = "⭐".repeat(rating) + "☆".repeat(10 - rating);
- // Комментарий
Map comments = new HashMap<>();
comments.put(10, "💯 ИДЕАЛЬНО!");
comments.put(9, "🔥 Почти идеально!");
@@ -92,12 +76,4 @@ public class RateCommand implements Command {
.build())
.queue();
}
-
- private static final Random RANDOM = new Random();
-
- private int getDeterministicValue(String userId, int salt, int min, int max) {
- long seed = Long.parseLong(userId) + salt;
- RANDOM.setSeed(seed);
- return RANDOM.nextInt(max - min + 1) + min;
- }
}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/RatingConfig.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/RatingConfig.java
new file mode 100644
index 0000000..fed7f33
--- /dev/null
+++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/RatingConfig.java
@@ -0,0 +1,81 @@
+package creator.loki.feature.entertainment.commands.rating;
+
+import java.awt.Color;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class RatingConfig {
+ private static final Map> LEVELS;
+
+ static {
+ var m = new LinkedHashMap>();
+ m.put(
+ "mustache",
+ List.of(
+ new RatingLevel(1000, "🎩 ЛЕГЕНДАРНЫЕ УСИЩИ!", new Color(255, 215, 0)),
+ new RatingLevel(80, "🧔 Роскошные усы!", new Color(139, 69, 19)),
+ new RatingLevel(60, "👨 Хорошие усы", new Color(160, 82, 45)),
+ new RatingLevel(40, "😊 Усики растут", new Color(205, 133, 63)),
+ new RatingLevel(20, "🙂 Лёгкая щетина", new Color(210, 180, 140)),
+ new RatingLevel(1, "😐 Почти нет усов", new Color(128, 128, 128)),
+ new RatingLevel(0, "🪒 Гладко выбрит", new Color(200, 200, 200))));
+ m.put(
+ "iq",
+ List.of(
+ new RatingLevel(140, "🧠 Гений!", new Color(255, 215, 0)),
+ new RatingLevel(120, "🎓 Очень умный", new Color(100, 255, 150)),
+ new RatingLevel(100, "📚 Средний", new Color(52, 152, 219)),
+ new RatingLevel(80, "📖 Ниже среднего", new Color(255, 165, 0)),
+ new RatingLevel(0, "🤔 Низкий", new Color(231, 76, 60))));
+ m.put(
+ "fat",
+ List.of(
+ new RatingLevel(90, "🐷 СВИНОТА!", new Color(255, 0, 0)),
+ new RatingLevel(70, "🍔 Очень жирный", new Color(255, 100, 0)),
+ new RatingLevel(50, "🍕 Жирноват", new Color(255, 165, 0)),
+ new RatingLevel(30, "👍 Нормальный вес", new Color(0, 200, 0)),
+ new RatingLevel(10, "🥗 Худой", new Color(100, 200, 255)),
+ new RatingLevel(0, "💀 Очень худой", new Color(150, 150, 150))));
+ m.put(
+ "pp",
+ List.of(
+ new RatingLevel(25, "😳 НЕВЕРОЯТНО!", new Color(255, 215, 0)),
+ new RatingLevel(20, "😮 Впечатляет", new Color(100, 255, 150)),
+ new RatingLevel(15, "👍 Выше среднего", new Color(52, 152, 219)),
+ new RatingLevel(10, "🙂 Средний", new Color(128, 128, 128)),
+ new RatingLevel(5, "😐 Ниже среднего", new Color(255, 165, 0)),
+ new RatingLevel(0, "💀 Микро", new Color(231, 76, 60))));
+ m.put(
+ "boykisser",
+ List.of(
+ new RatingLevel(90, "💋 ЛЕГЕНДАРНЫЙ BOYKISSER!", new Color(255, 105, 180)),
+ new RatingLevel(70, "😘 Активный boykisser", new Color(255, 182, 193)),
+ new RatingLevel(50, "😊 Любит целоваться", new Color(255, 192, 203)),
+ new RatingLevel(30, "🙂 Иногда целуется", new Color(255, 218, 185)),
+ new RatingLevel(10, "😐 Редко целуется", new Color(176, 196, 222)),
+ new RatingLevel(0, "😶 Не целуется", new Color(128, 128, 128))));
+ LEVELS = Collections.unmodifiableMap(m);
+ }
+
+ public static List getMustacheLevels() {
+ return LEVELS.get("mustache");
+ }
+
+ public static List getIqLevels() {
+ return LEVELS.get("iq");
+ }
+
+ public static List getFatLevels() {
+ return LEVELS.get("fat");
+ }
+
+ public static List getPpLevels() {
+ return LEVELS.get("pp");
+ }
+
+ public static List getBoykisserLevels() {
+ return LEVELS.get("boykisser");
+ }
+}
diff --git a/src/main/java/creator/loki/feature/entertainment/commands/RatingLevel.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/RatingLevel.java
similarity index 95%
rename from src/main/java/creator/loki/feature/entertainment/commands/RatingLevel.java
rename to src/main/java/creator/loki/feature/entertainment/commands/rating/RatingLevel.java
index d507486..54821df 100644
--- a/src/main/java/creator/loki/feature/entertainment/commands/RatingLevel.java
+++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/RatingLevel.java
@@ -1,4 +1,4 @@
-package creator.loki.feature.entertainment.commands;
+package creator.loki.feature.entertainment.commands.rating;
import java.awt.Color;
import java.util.List;
diff --git a/src/main/java/creator/loki/feature/entertainment/service/RatingService.java b/src/main/java/creator/loki/feature/entertainment/service/RatingService.java
new file mode 100644
index 0000000..e4f2e82
--- /dev/null
+++ b/src/main/java/creator/loki/feature/entertainment/service/RatingService.java
@@ -0,0 +1,47 @@
+package creator.loki.feature.entertainment.service;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Random;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import creator.loki.command.CommandContext;
+import creator.loki.core.security.CooldownManager;
+import net.dv8tion.jda.api.entities.Member;
+
+@Singleton
+public class RatingService {
+ private static final Random RANDOM = new Random();
+ private static final Duration COOLDOWN = Duration.ofSeconds(5);
+
+ private final CooldownManager cooldownManager;
+
+ @Inject
+ public RatingService(CooldownManager cooldownManager) {
+ this.cooldownManager = cooldownManager;
+ }
+
+ public int getDeterministicValue(String userId, int salt, int min, int max) {
+ long seed = Long.parseLong(userId) + salt;
+ RANDOM.setSeed(seed);
+ return RANDOM.nextInt(max - min + 1) + min;
+ }
+
+ public Member resolveTarget(CommandContext context) {
+ List mentioned = context.getMessage().getMentions().getMembers();
+ return mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
+ }
+
+ public boolean checkAndApplyCooldown(CommandContext context, String commandName) {
+ if (cooldownManager.isOnCooldown(context.getAuthor().getId(), commandName)) {
+ long remaining =
+ cooldownManager.getRemainingSeconds(context.getAuthor().getId(), commandName);
+ context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
+ return false;
+ }
+ cooldownManager.applyCooldown(
+ context.getAuthor().getId(), commandName, (int) COOLDOWN.toSeconds());
+ return true;
+ }
+}
diff --git a/src/main/java/creator/loki/feature/help/HelpMenuHandler.java b/src/main/java/creator/loki/feature/help/HelpMenuHandler.java
index e8aef4c..fff3579 100644
--- a/src/main/java/creator/loki/feature/help/HelpMenuHandler.java
+++ b/src/main/java/creator/loki/feature/help/HelpMenuHandler.java
@@ -13,10 +13,10 @@ import creator.loki.core.registry.CommandRegistry;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.HelpCategories;
import net.dv8tion.jda.api.Permission;
-import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
-import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.components.actionrow.ActionRow;
import net.dv8tion.jda.api.components.selections.StringSelectMenu;
+import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
+import net.dv8tion.jda.api.hooks.ListenerAdapter;
@Singleton
public class HelpMenuHandler extends ListenerAdapter {
diff --git a/src/main/java/creator/loki/feature/help/commands/HelpCommand.java b/src/main/java/creator/loki/feature/help/commands/HelpCommand.java
index d4c7949..eee9237 100644
--- a/src/main/java/creator/loki/feature/help/commands/HelpCommand.java
+++ b/src/main/java/creator/loki/feature/help/commands/HelpCommand.java
@@ -12,9 +12,9 @@ import creator.loki.core.registry.CommandRegistry;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.HelpCategories;
import net.dv8tion.jda.api.Permission;
-import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.components.actionrow.ActionRow;
import net.dv8tion.jda.api.components.selections.StringSelectMenu;
+import net.dv8tion.jda.api.entities.User;
public class HelpCommand implements Command {
private final CommandRegistry registry;
@@ -75,7 +75,11 @@ public class HelpCommand implements Command {
net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode(emoji));
});
- context.getChannel().sendMessageEmbeds(embed.build()).addComponents(ActionRow.of(menuBuilder.build())).queue();
+ context
+ .getChannel()
+ .sendMessageEmbeds(embed.build())
+ .addComponents(ActionRow.of(menuBuilder.build()))
+ .queue();
}
private EmbedFactory createMainPage(
diff --git a/src/main/java/creator/loki/feature/level/MessageListener.java b/src/main/java/creator/loki/feature/level/MessageListener.java
index 8cdebf8..23c07c4 100644
--- a/src/main/java/creator/loki/feature/level/MessageListener.java
+++ b/src/main/java/creator/loki/feature/level/MessageListener.java
@@ -5,7 +5,6 @@ import java.util.concurrent.ThreadLocalRandom;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.core.EventDispatcher;
-import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.level.domain.LevelUpResult;
import creator.loki.feature.level.service.LevelService;
@@ -20,13 +19,16 @@ public class MessageListener extends ListenerAdapter {
private final EventDispatcher dispatcher;
private final LevelService levelService;
private final EconomyService economyService;
+ private final LevelUpNotifier levelUpNotifier;
@Inject
public MessageListener(
- EventDispatcher dispatcher, LevelService levelService, EconomyService economyService) {
+ EventDispatcher dispatcher, LevelService levelService,
+ EconomyService economyService, LevelUpNotifier levelUpNotifier) {
this.dispatcher = dispatcher;
this.levelService = levelService;
this.economyService = economyService;
+ this.levelUpNotifier = levelUpNotifier;
}
@Override
@@ -35,19 +37,10 @@ public class MessageListener extends ListenerAdapter {
if (!event.getAuthor().isBot()) {
String userId = event.getAuthor().getId();
LevelUpResult levelUp = levelService.addMessageXP(userId);
- if (levelUp != null) {
- event
- .getChannel()
- .sendMessageEmbeds(
- EmbedTemplates.success(
- "🎉 Повышение уровня!",
- event.getAuthor().getAsMention()
- + " достиг **"
- + levelUp.getNewLevel()
- + " уровня**! Всего XP: **"
- + levelUp.getTotalXP()
- + "**"))
- .queue();
+ if (levelUp != null && event.getMember() != null) {
+ levelUpNotifier.sendLevelUpNotification(
+ event.getMember(), levelUp.getOldLevel(), levelUp.getNewLevel(),
+ event.getChannel());
}
int lcAmount = ThreadLocalRandom.current().nextInt(1, 4);
economyService.earnCash(userId, lcAmount);
diff --git a/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java b/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java
index e4c4bc6..c48a71f 100644
--- a/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java
+++ b/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java
@@ -11,10 +11,12 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/** Обработчик кнопок подтверждения сброса уровней */
@Singleton
public class ResetLevelsButtonHandler extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(ResetLevelsButtonHandler.class);
+ private static final String PREFIX_CONFIRM = "reset_levels_confirm:";
+ private static final String PREFIX_CANCEL = "reset_levels_cancel:";
+
private final ResetLevelsCommand resetCommand;
@Inject
@@ -26,87 +28,47 @@ public class ResetLevelsButtonHandler extends ListenerAdapter {
public void onButtonInteraction(ButtonInteractionEvent event) {
String buttonId = event.getComponentId();
- // Проверяем что это наша кнопка
- if (!buttonId.startsWith("reset_levels_")) {
- return;
- }
-
- // Извлекаем ID пользователя из кнопки
- String[] parts = buttonId.split(":");
- if (parts.length != 2) {
- return;
- }
-
- String authorId = parts[1];
-
- // Проверяем что кнопку нажал тот же пользователь
- if (!event.getUser().getId().equals(authorId)) {
- event
- .reply("❌ Только инициатор команды может подтвердить действие!")
- .setEphemeral(true)
- .queue();
- return;
- }
-
- if (buttonId.startsWith("reset_levels_confirm:")) {
- // Подтверждение
+ if (buttonId.startsWith(PREFIX_CONFIRM)) {
+ String authorId = buttonId.substring(PREFIX_CONFIRM.length());
+ if (authorId.isEmpty()) return;
+ if (!event.getUser().getId().equals(authorId)) {
+ event.reply("❌ Только инициатор команды может подтвердить действие!").setEphemeral(true).queue();
+ return;
+ }
event.deferReply().queue();
-
try {
- String guildId = event.getGuild().getId();
- String userId = event.getUser().getId();
-
- // Выполняем сброс
- resetCommand.executeReset(guildId, userId);
-
- // Удаляем исходное сообщение с кнопками
+ resetCommand.executeReset(event.getGuild().getId(), event.getUser().getId());
event.getMessage().delete().queue();
-
- // Отправляем подтверждение
- event
- .getHook()
- .sendMessageEmbeds(
- EmbedFactory.create()
- .withTitle("✅ Сброс уровней выполнен")
- .withDescription(
- "Уровни и XP всех пользователей успешно сброшены!\n\n"
- + "**Сохранено:**\n"
- + "• Время в голосовых каналах\n"
- + "• Количество сообщений\n"
- + "• Репутация\n\n"
- + "**Сброшено:**\n"
- + "• Уровень → 0\n"
- + "• XP → 0\n\n"
- + "Теперь используется новая система уровней!")
- .withColor(Color.GREEN)
- .withFooter(
- "Инициатор: " + event.getUser().getName(), event.getUser().getAvatarUrl())
- .build())
- .queue();
-
- LoggerDecorator.bot(
- logger, "Сброс уровней подтверждён пользователем: " + event.getUser().getName());
-
+ event.getHook().sendMessageEmbeds(
+ EmbedFactory.create()
+ .withTitle("✅ Сброс уровней выполнен")
+ .withDescription("Уровни и XP всех пользователей успешно сброшены!\n\n"
+ + "**Сохранено:**\n• Время в голосовых каналах\n"
+ + "• Количество сообщений\n• Репутация\n\n"
+ + "**Сброшено:**\n• Уровень → 0\n• XP → 0\n\n"
+ + "Теперь используется новая система уровней!")
+ .withColor(Color.GREEN)
+ .withFooter("Инициатор: " + event.getUser().getName(), event.getUser().getAvatarUrl())
+ .build()).queue();
+ LoggerDecorator.bot(logger, "Сброс уровней подтверждён: " + event.getUser().getName());
} catch (Exception e) {
LoggerDecorator.error(logger, "Ошибка при выполнении сброса", e);
- event
- .getHook()
- .sendMessageEmbeds(
- EmbedFactory.create()
- .withTitle("❌ Ошибка")
- .withDescription("Произошла ошибка при сбросе уровней: " + e.getMessage())
- .withColor(Color.RED)
- .build())
- .queue();
+ event.getHook().sendMessageEmbeds(
+ EmbedFactory.create()
+ .withTitle("❌ Ошибка")
+ .withDescription("Произошла ошибка при сбросе уровней: " + e.getMessage())
+ .withColor(Color.RED).build()).queue();
+ }
+ } else if (buttonId.startsWith(PREFIX_CANCEL)) {
+ String authorId = buttonId.substring(PREFIX_CANCEL.length());
+ if (authorId.isEmpty()) return;
+ if (!event.getUser().getId().equals(authorId)) {
+ event.reply("❌ Только инициатор команды может отменить действие!").setEphemeral(true).queue();
+ return;
}
-
- } else if (buttonId.startsWith("reset_levels_cancel:")) {
- // Отмена
event.getMessage().delete().queue();
event.reply("❌ Сброс уровней отменён.").setEphemeral(true).queue();
-
- LoggerDecorator.bot(
- logger, "Сброс уровней отменён пользователем: " + event.getUser().getName());
+ LoggerDecorator.bot(logger, "Сброс уровней отменён: " + event.getUser().getName());
}
}
}
diff --git a/src/main/java/creator/loki/feature/level/VoiceTracker.java b/src/main/java/creator/loki/feature/level/VoiceTracker.java
index f3a11c9..f8be26a 100644
--- a/src/main/java/creator/loki/feature/level/VoiceTracker.java
+++ b/src/main/java/creator/loki/feature/level/VoiceTracker.java
@@ -9,6 +9,7 @@ import java.util.concurrent.TimeUnit;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.feature.economy.service.EconomyService;
+import creator.loki.feature.level.domain.LevelUpResult;
import creator.loki.feature.level.service.LevelService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
@@ -16,21 +17,22 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/** Отслеживание времени в голосовых каналах */
@Singleton
public class VoiceTracker extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(VoiceTracker.class);
private final LevelService levelService;
private final EconomyService economyService;
+ private final LevelUpNotifier levelUpNotifier;
private final Map voiceJoinTimes = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
+ private final Object lock = new Object();
@Inject
- public VoiceTracker(LevelService levelService, EconomyService economyService) {
+ public VoiceTracker(LevelService levelService, EconomyService economyService,
+ LevelUpNotifier levelUpNotifier) {
this.levelService = levelService;
this.economyService = economyService;
-
- // Каждую минуту начисляем XP тем, кто в войсе
+ this.levelUpNotifier = levelUpNotifier;
scheduler.scheduleAtFixedRate(
() -> Thread.startVirtualThread(this::updateVoiceTime), 1, 1, TimeUnit.MINUTES);
}
@@ -39,66 +41,48 @@ public class VoiceTracker extends ListenerAdapter {
public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) {
Member member = event.getMember();
String userId = member.getId();
+ if (member.getUser().isBot()) return;
- // Игнорируем ботов
- if (member.getUser().isBot()) {
- return;
- }
-
- // Пользователь зашёл в войс
if (event.getChannelJoined() != null && event.getChannelLeft() == null) {
voiceJoinTimes.put(userId, System.currentTimeMillis());
- logger.debug("Пользователь {} зашёл в войс", member.getEffectiveName());
} else if (event.getChannelLeft() != null && event.getChannelJoined() == null) {
- // Пользователь вышел из войса
- Long joinTime = voiceJoinTimes.remove(userId);
+ Long joinTime;
+ synchronized (lock) { joinTime = voiceJoinTimes.remove(userId); }
if (joinTime != null) {
- long timeSpent = System.currentTimeMillis() - joinTime;
- long minutes = TimeUnit.MILLISECONDS.toMinutes(timeSpent);
-
+ long minutes = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - joinTime);
if (minutes > 0) {
- levelService.addVoiceXP(userId, minutes);
+ LevelUpResult lr = levelService.addVoiceXP(userId, minutes);
economyService.earnCash(userId, minutes / 2);
- logger.debug(
- "Пользователь {} провёл в войсе {} минут", member.getEffectiveName(), minutes);
+ if (lr != null && event.getMember() != null) {
+ levelUpNotifier.sendLevelUpNotification(
+ event.getMember(), lr.getOldLevel(), lr.getNewLevel(),
+ event.getGuild().getSystemChannel());
+ }
}
}
} else if (event.getChannelJoined() != null && event.getChannelLeft() != null) {
- // Пользователь переключился между каналами
- // Обновляем время входа
- voiceJoinTimes.put(userId, System.currentTimeMillis());
+ synchronized (lock) { voiceJoinTimes.put(userId, System.currentTimeMillis()); }
}
}
- /** Периодическое обновление времени для тех, кто долго в войсе */
private void updateVoiceTime() {
long now = System.currentTimeMillis();
-
- for (Map.Entry entry : voiceJoinTimes.entrySet()) {
- String userId = entry.getKey();
- long joinTime = entry.getValue();
- long timeSpent = now - joinTime;
- long minutes = TimeUnit.MILLISECONDS.toMinutes(timeSpent);
-
- if (minutes >= 1) {
- // Начисляем XP за каждую минуту
- levelService.addVoiceXP(userId, 1);
- economyService.earnCash(userId, 1);
- // Обновляем время входа
- voiceJoinTimes.put(userId, now);
+ synchronized (lock) {
+ for (Map.Entry entry : voiceJoinTimes.entrySet()) {
+ String userId = entry.getKey();
+ long minutes = TimeUnit.MILLISECONDS.toMinutes(now - entry.getValue());
+ if (minutes >= 1) {
+ levelService.addVoiceXP(userId, 1);
+ economyService.earnCash(userId, 1);
+ entry.setValue(now);
+ }
}
}
}
public void shutdown() {
scheduler.shutdown();
- try {
- if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
- scheduler.shutdownNow();
- }
- } catch (InterruptedException e) {
- scheduler.shutdownNow();
- Thread.currentThread().interrupt();
- }
+ try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) scheduler.shutdownNow(); }
+ catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); }
}
}
diff --git a/src/main/java/creator/loki/feature/level/service/LevelService.java b/src/main/java/creator/loki/feature/level/service/LevelService.java
index c237170..d527d28 100644
--- a/src/main/java/creator/loki/feature/level/service/LevelService.java
+++ b/src/main/java/creator/loki/feature/level/service/LevelService.java
@@ -36,6 +36,7 @@ public class LevelService {
return null;
}
+ cooldownService.markMessageSent(userId);
UserStats stats = repository.getUserStats(userId);
long now = System.currentTimeMillis();
int oldLevel = stats.getLevel();
diff --git a/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java b/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java
index 7db7a9c..3c27581 100644
--- a/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java
+++ b/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java
@@ -1,55 +1,45 @@
package creator.loki.feature.level.service;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
import creator.loki.feature.level.domain.UserStats;
import creator.loki.feature.level.repository.UserStatsRepository;
-/**
- * Сервис для управления кулдауном сообщений Использует персистентное хранение через
- * UserStats.lastMessageTime
- */
public class MessageCooldownService {
- private static final long MESSAGE_COOLDOWN_MS = 60000; // 1 минута
+ private static final long MESSAGE_COOLDOWN_MS = 60000;
private final UserStatsRepository repository;
+ private final Map lastMessageTimes = new ConcurrentHashMap<>();
public MessageCooldownService(UserStatsRepository repository) {
this.repository = repository;
}
- /**
- * Проверить, может ли пользователь получить XP за сообщение
- *
- * @param userId ID пользователя
- * @return true если кулдаун прошел
- */
public boolean canReceiveXP(String userId) {
- UserStats stats = repository.getUserStats(userId);
long now = System.currentTimeMillis();
- long lastMessage = stats.getLastMessageTime();
-
- return lastMessage == 0 || (now - lastMessage) >= MESSAGE_COOLDOWN_MS;
+ Long lastTime = lastMessageTimes.get(userId);
+ if (lastTime != null && (now - lastTime) < MESSAGE_COOLDOWN_MS) return false;
+ if (lastTime != null) return true;
+ long dbTime = repository.getUserStats(userId).getLastMessageTime();
+ if (dbTime != 0) lastMessageTimes.put(userId, dbTime);
+ return dbTime == 0 || (now - dbTime) >= MESSAGE_COOLDOWN_MS;
+ }
+
+ public void markMessageSent(String userId) {
+ lastMessageTimes.put(userId, System.currentTimeMillis());
}
- /**
- * Получить оставшееся время кулдауна в секундах
- *
- * @param userId ID пользователя
- * @return секунды до окончания кулдауна, 0 если кулдаун прошел
- */
public long getRemainingCooldown(String userId) {
- UserStats stats = repository.getUserStats(userId);
long now = System.currentTimeMillis();
- long lastMessage = stats.getLastMessageTime();
-
- if (lastMessage == 0) {
- return 0;
+ Long lastTime = lastMessageTimes.get(userId);
+ if (lastTime == null) {
+ long dbTime = repository.getUserStats(userId).getLastMessageTime();
+ if (dbTime == 0) return 0;
+ lastMessageTimes.put(userId, dbTime);
+ lastTime = dbTime;
}
-
- long elapsed = now - lastMessage;
- if (elapsed >= MESSAGE_COOLDOWN_MS) {
- return 0;
- }
-
- return (MESSAGE_COOLDOWN_MS - elapsed) / 1000;
+ long elapsed = now - lastTime;
+ return elapsed >= MESSAGE_COOLDOWN_MS ? 0 : (MESSAGE_COOLDOWN_MS - elapsed) / 1000;
}
}
diff --git a/src/main/java/creator/loki/feature/metrics/MetricsCollector.java b/src/main/java/creator/loki/feature/metrics/MetricsCollector.java
index fef7e72..6c2f14e 100644
--- a/src/main/java/creator/loki/feature/metrics/MetricsCollector.java
+++ b/src/main/java/creator/loki/feature/metrics/MetricsCollector.java
@@ -10,16 +10,10 @@ import creator.loki.util.LoggerDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/** Сборщик метрик для мониторинга бота */
public class MetricsCollector {
private static final Logger logger = LoggerFactory.getLogger(MetricsCollector.class);
- // Счётчики команд
- private final ConcurrentHashMap commandCounts = new ConcurrentHashMap<>();
- private final ConcurrentHashMap commandErrors = new ConcurrentHashMap<>();
- private final ConcurrentHashMap commandTotalTime = new ConcurrentHashMap<>();
-
- // Общие метрики
+ private final ConcurrentHashMap commands = new ConcurrentHashMap<>();
private final LongAdder totalCommands = new LongAdder();
private final LongAdder totalErrors = new LongAdder();
private final AtomicLong startTime = new AtomicLong(System.currentTimeMillis());
@@ -28,79 +22,60 @@ public class MetricsCollector {
LoggerDecorator.bot(logger, "MetricsCollector инициализирован");
}
- /** Записывает выполнение команды */
public void recordCommand(String commandName, long executionTimeMs) {
- commandCounts.computeIfAbsent(commandName, k -> new LongAdder()).increment();
- commandTotalTime.computeIfAbsent(commandName, k -> new AtomicLong()).addAndGet(executionTimeMs);
+ var entry = commands.computeIfAbsent(commandName, k -> new CommandEntry());
+ entry.count.increment();
+ entry.totalTime.addAndGet(executionTimeMs);
totalCommands.increment();
}
- /** Записывает ошибку команды */
public void recordError(String commandName) {
- commandErrors.computeIfAbsent(commandName, k -> new LongAdder()).increment();
+ var entry = commands.computeIfAbsent(commandName, k -> new CommandEntry());
+ entry.errors.increment();
totalErrors.increment();
}
- /** Возвращает количество выполнений команды */
public long getCommandCount(String commandName) {
- LongAdder counter = commandCounts.get(commandName);
- return counter != null ? counter.sum() : 0;
+ var entry = commands.get(commandName);
+ return entry != null ? entry.count.sum() : 0;
}
- /** Возвращает количество ошибок команды */
public long getCommandErrors(String commandName) {
- LongAdder counter = commandErrors.get(commandName);
- return counter != null ? counter.sum() : 0;
+ var entry = commands.get(commandName);
+ return entry != null ? entry.errors.sum() : 0;
}
- /** Возвращает среднее время выполнения команды */
public double getAverageExecutionTime(String commandName) {
- long count = getCommandCount(commandName);
- if (count == 0) {
- return 0.0;
- }
- AtomicLong totalTime = commandTotalTime.get(commandName);
- if (totalTime == null) {
- return 0.0;
- }
- return (double) totalTime.get() / count;
+ var entry = commands.get(commandName);
+ var count = entry != null ? entry.count.sum() : 0;
+ return count == 0 ? 0.0 : (double) entry.totalTime.get() / count;
}
- /** Возвращает общее количество выполненных команд */
public long getTotalCommands() {
return totalCommands.sum();
}
- /** Возвращает общее количество ошибок */
public long getTotalErrors() {
return totalErrors.sum();
}
- /** Возвращает процент ошибок */
public double getErrorRate() {
- long total = getTotalCommands();
- if (total == 0) {
- return 0.0;
- }
- return (double) getTotalErrors() / total * 100;
+ return getTotalCommands() == 0 ? 0.0 : (double) getTotalErrors() / getTotalCommands() * 100;
}
- /** Возвращает время работы бота в секундах */
public long getUptimeSeconds() {
return (System.currentTimeMillis() - startTime.get()) / 1000;
}
- /** JSON для {@code GET /health} (пробы оркестраторов). */
public String buildHealthJson() {
- String version =
- Optional.ofNullable(MetricsCollector.class.getPackage().getImplementationVersion())
- .filter(v -> !v.isBlank())
- .orElse("dev");
return String.format(
Locale.ROOT,
"{\"status\":\"ok\",\"version\":%s,\"uptimeSeconds\":%d,\"commandsRun\":%d,"
+ "\"errors\":%d,\"errorRatePercent\":%.4f}",
- jsonQuoted(version),
+ jsonQuoted(
+ Optional.ofNullable(MetricsCollector.class.getPackage().getImplementationVersion())
+ .filter(v -> !v.isBlank())
+ .orElse("dev")),
getUptimeSeconds(),
getTotalCommands(),
getTotalErrors(),
@@ -108,75 +83,61 @@ public class MetricsCollector {
}
private static String jsonQuoted(String s) {
- StringBuilder sb = new StringBuilder(s.length() + 2);
- sb.append('"');
- for (int i = 0; i < s.length(); i++) {
- char c = s.charAt(i);
- switch (c) {
- case '\\' -> sb.append("\\\\");
- case '"' -> sb.append("\\\"");
- case '\n' -> sb.append("\\n");
- case '\r' -> sb.append("\\r");
- case '\t' -> sb.append("\\t");
- default -> sb.append(c < ' ' ? ' ' : c);
- }
- }
- sb.append('"');
- return sb.toString();
+ return '"'
+ + s.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\t", "\\t")
+ + '"';
}
- /** Возвращает топ команд по использованию */
public String getTopCommands(int limit) {
- StringBuilder sb = new StringBuilder();
+ var sb = new StringBuilder();
sb.append("📊 Топ команд:\n");
-
- commandCounts.entrySet().stream()
- .sorted((e1, e2) -> Long.compare(e2.getValue().sum(), e1.getValue().sum()))
+ commands.entrySet().stream()
+ .sorted((e1, e2) -> Long.compare(e2.getValue().count.sum(), e1.getValue().count.sum()))
.limit(limit)
.forEach(
entry -> {
- String cmd = entry.getKey();
- long count = entry.getValue().sum();
- long errors = getCommandErrors(cmd);
- double avgTime = getAverageExecutionTime(cmd);
-
+ var ce = entry.getValue();
+ var count = ce.count.sum();
+ var avgTime = count == 0 ? 0.0 : (double) ce.totalTime.get() / count;
sb.append(
String.format(
" %s: %d выполнений, %.1f мс среднее, %d ошибок%n",
- cmd, count, avgTime, errors));
+ entry.getKey(), count, avgTime, ce.errors.sum()));
});
-
return sb.toString();
}
- /** Возвращает полную статистику */
public String getFullStats() {
- long uptime = getUptimeSeconds();
- long hours = uptime / 3600;
- long minutes = (uptime % 3600) / 60;
- long seconds = uptime % 60;
-
- StringBuilder sb = new StringBuilder();
+ var sb = new StringBuilder();
sb.append("📈 Статистика бота\n");
sb.append("━━━━━━━━━━━━━━━━━━━━\n");
- sb.append(String.format("⏱️ Uptime: %dч %dм %dс%n", hours, minutes, seconds));
+ sb.append(
+ String.format(
+ "⏱️ Uptime: %dч %dм %dс%n",
+ getUptimeSeconds() / 3600, (getUptimeSeconds() % 3600) / 60, getUptimeSeconds() % 60));
sb.append(String.format("📝 Всего команд: %d%n", getTotalCommands()));
sb.append(String.format("❌ Всего ошибок: %d (%.2f%%)%n", getTotalErrors(), getErrorRate()));
- sb.append(String.format("📊 Уникальных команд: %d%n", commandCounts.size()));
+ sb.append(String.format("📊 Уникальных команд: %d%n", commands.size()));
sb.append("\n");
sb.append(getTopCommands(10));
-
return sb.toString();
}
- /** Сбрасывает все метрики */
public void reset() {
- commandCounts.clear();
- commandErrors.clear();
- commandTotalTime.clear();
+ commands.clear();
totalCommands.reset();
totalErrors.reset();
startTime.set(System.currentTimeMillis());
LoggerDecorator.bot(logger, "Метрики сброшены");
}
+
+ private static class CommandEntry {
+ final LongAdder count = new LongAdder();
+ final LongAdder errors = new LongAdder();
+ final AtomicLong totalTime = new AtomicLong();
+ }
}
diff --git a/src/main/java/creator/loki/feature/moderation/ModerationCommandRegistrar.java b/src/main/java/creator/loki/feature/moderation/ModerationCommandRegistrar.java
index 4306de7..fc74270 100644
--- a/src/main/java/creator/loki/feature/moderation/ModerationCommandRegistrar.java
+++ b/src/main/java/creator/loki/feature/moderation/ModerationCommandRegistrar.java
@@ -6,12 +6,13 @@ import creator.loki.core.registry.CommandRegistry;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.config.repository.GuildConfigRepository;
import creator.loki.feature.logging.LogSetupPrefixCommand;
+import creator.loki.feature.moderation.commands.caselookup.CaseLookupCommand;
import creator.loki.feature.moderation.commands.punish.MuteCommand;
import creator.loki.feature.moderation.commands.punish.PunishCommand;
import creator.loki.feature.moderation.commands.punish.UnmuteCommand;
+import creator.loki.feature.moderation.commands.swap.SwapCommand;
import creator.loki.feature.moderation.commands.utils.ClearCommand;
import creator.loki.feature.moderation.commands.utils.SlowmodeCommand;
-import creator.loki.feature.moderation.commands.utils.SwapAndCaseCommand;
import creator.loki.feature.moderation.commands.warn.UnwarnCommand;
import creator.loki.feature.moderation.commands.warn.WarnCommand;
import creator.loki.feature.moderation.commands.warn.WarningsHistoryCommand;
@@ -84,8 +85,8 @@ public class ModerationCommandRegistrar implements CommandRegistrar {
registry.register(new WarningsHistoryCommand(warningRepository));
registry.register(new ClearCommand());
registry.register(new SlowmodeCommand());
- registry.register(SwapAndCaseCommand.createSwap());
- registry.register(SwapAndCaseCommand.createCaseLookup(moderationCaseRepository));
+ registry.register(new SwapCommand());
+ registry.register(new CaseLookupCommand(moderationCaseRepository));
registry.register(new CreateRoleCommand(roleManager));
registry.register(new DeleteRoleCommand(roleManager));
diff --git a/src/main/java/creator/loki/feature/moderation/commands/caselookup/CaseLookupCommand.java b/src/main/java/creator/loki/feature/moderation/commands/caselookup/CaseLookupCommand.java
new file mode 100644
index 0000000..97e8be3
--- /dev/null
+++ b/src/main/java/creator/loki/feature/moderation/commands/caselookup/CaseLookupCommand.java
@@ -0,0 +1,80 @@
+package creator.loki.feature.moderation.commands.caselookup;
+
+import java.awt.Color;
+
+import creator.loki.command.Command;
+import creator.loki.command.CommandContext;
+import creator.loki.embed.EmbedFactory;
+import creator.loki.feature.moderation.repository.ModerationCaseRepository;
+import creator.loki.feature.moderation.util.ModerationHelper;
+import net.dv8tion.jda.api.Permission;
+
+public class CaseLookupCommand implements Command {
+ private final ModerationCaseRepository cases;
+
+ public CaseLookupCommand(ModerationCaseRepository cr) {
+ cases = cr;
+ }
+
+ @Override
+ public String getName() {
+ return "case";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Показать кейс модерации по ID";
+ }
+
+ @Override
+ public String getUsage() {
+ return "case ";
+ }
+
+ @Override
+ public String getCategory() {
+ return "Модерация";
+ }
+
+ @Override
+ public void execute(CommandContext ctx) {
+ var g = ctx.getGuild();
+ if (g == null) {
+ ModerationHelper.error(ctx, "Команда только на сервере");
+ return;
+ }
+ var mod = ctx.getAuthor();
+ if (mod == null || !mod.hasPermission(Permission.KICK_MEMBERS)) {
+ ModerationHelper.error(ctx, "Нужно **Kick Members** или выше");
+ return;
+ }
+ if (ctx.getArgs().isEmpty()) {
+ ModerationHelper.error(ctx, "Использование: `" + getUsage() + "`");
+ return;
+ }
+ long caseId;
+ try {
+ caseId = Long.parseLong(ctx.getArgs().get(0).trim());
+ } catch (NumberFormatException e) {
+ ModerationHelper.error(ctx, "ID кейса должен быть числом");
+ return;
+ }
+ cases
+ .findById(g.getId(), caseId)
+ .ifPresentOrElse(
+ c ->
+ ctx.getChannel()
+ .sendMessageEmbeds(
+ EmbedFactory.create()
+ .withTitle("Кейс #" + c.id())
+ .withColor(Color.BLUE)
+ .addField("Действие", "`" + c.action() + "`", true)
+ .addField("Цель", "<@" + c.targetId() + ">", true)
+ .addField("Модератор", "<@" + c.moderatorId() + ">", true)
+ .addField("Причина", ModerationHelper.trunc(c.reason(), 1000), false)
+ .addField("Время", "", false)
+ .build())
+ .queue(),
+ () -> ModerationHelper.error(ctx, "Кейса `" + caseId + "` на этом сервере нет"));
+ }
+}
diff --git a/src/main/java/creator/loki/feature/moderation/commands/punish/MuteCommand.java b/src/main/java/creator/loki/feature/moderation/commands/punish/MuteCommand.java
index 392f4b0..b77e48e 100644
--- a/src/main/java/creator/loki/feature/moderation/commands/punish/MuteCommand.java
+++ b/src/main/java/creator/loki/feature/moderation/commands/punish/MuteCommand.java
@@ -6,8 +6,8 @@ import java.time.OffsetDateTime;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
-import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.config.repository.ConfigRepository;
+import creator.loki.feature.moderation.util.ModerationHelper;
import creator.loki.feature.moderation.MuteRoleService;
import creator.loki.feature.moderation.MuteScheduler;
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
@@ -190,10 +190,10 @@ public class MuteCommand implements Command {
}
private static String sub(String s, int max) {
- return s.length() <= max ? s : s.substring(0, max - 1) + "…";
+ return ModerationHelper.trunc(s, max);
}
private static void err(CommandContext ctx, String msg) {
- ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
+ ModerationHelper.error(ctx, msg);
}
}
diff --git a/src/main/java/creator/loki/feature/moderation/commands/punish/PunishCommand.java b/src/main/java/creator/loki/feature/moderation/commands/punish/PunishCommand.java
index 25483f4..f0eaed7 100644
--- a/src/main/java/creator/loki/feature/moderation/commands/punish/PunishCommand.java
+++ b/src/main/java/creator/loki/feature/moderation/commands/punish/PunishCommand.java
@@ -5,6 +5,7 @@ import java.util.concurrent.TimeUnit;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedTemplates;
+import creator.loki.feature.moderation.util.ModerationHelper;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
@@ -123,6 +124,6 @@ public final class PunishCommand {
}
private static void err(CommandContext ctx, String msg) {
- ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
+ ModerationHelper.error(ctx, msg);
}
}
diff --git a/src/main/java/creator/loki/feature/moderation/commands/punish/UnmuteCommand.java b/src/main/java/creator/loki/feature/moderation/commands/punish/UnmuteCommand.java
index aafcc93..9bec7af 100644
--- a/src/main/java/creator/loki/feature/moderation/commands/punish/UnmuteCommand.java
+++ b/src/main/java/creator/loki/feature/moderation/commands/punish/UnmuteCommand.java
@@ -13,6 +13,7 @@ import creator.loki.feature.moderation.MuteRoleService;
import creator.loki.feature.moderation.MuteScheduler;
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
import creator.loki.feature.moderation.repository.ModerationCaseRepository;
+import creator.loki.feature.moderation.util.ModerationHelper;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
@@ -137,10 +138,10 @@ public class UnmuteCommand implements Command {
}
private static String trunc(String s, int max) {
- return s.length() <= max ? s : s.substring(0, max - 1) + "…";
+ return ModerationHelper.trunc(s, max);
}
private static void err(CommandContext ctx, String msg) {
- ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
+ ModerationHelper.error(ctx, msg);
}
}
diff --git a/src/main/java/creator/loki/feature/moderation/commands/swap/SwapCommand.java b/src/main/java/creator/loki/feature/moderation/commands/swap/SwapCommand.java
new file mode 100644
index 0000000..b6a8ddf
--- /dev/null
+++ b/src/main/java/creator/loki/feature/moderation/commands/swap/SwapCommand.java
@@ -0,0 +1,97 @@
+package creator.loki.feature.moderation.commands.swap;
+
+import java.util.Random;
+
+import creator.loki.command.Command;
+import creator.loki.command.CommandContext;
+import creator.loki.embed.EmbedTemplates;
+import creator.loki.feature.moderation.util.ModerationHelper;
+import net.dv8tion.jda.api.Permission;
+
+public class SwapCommand implements Command {
+ private final Random random = new Random();
+
+ @Override
+ public String getName() {
+ return "swap";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Перекидывает пользователя между случайными голосовыми каналами";
+ }
+
+ @Override
+ public String getUsage() {
+ return "swap <@user> <количество 1-5>";
+ }
+
+ @Override
+ public String getCategory() {
+ return "Модерация";
+ }
+
+ @Override
+ public void execute(CommandContext ctx) {
+ var g = ctx.getGuild();
+ if (g == null) {
+ ModerationHelper.error(ctx, "Команда доступна только на сервере");
+ return;
+ }
+ var mod = ctx.getAuthor();
+ if (mod == null || !mod.hasPermission(Permission.VOICE_MOVE_OTHERS)) {
+ ModerationHelper.error(ctx, "Нет прав на перемещение");
+ return;
+ }
+ if (!g.getMember(g.getJDA().getSelfUser()).hasPermission(Permission.VOICE_MOVE_OTHERS)) {
+ ModerationHelper.error(ctx, "У бота нет прав на перемещение");
+ return;
+ }
+ var mentioned = ctx.getMessage().getMentions().getMembers();
+ if (mentioned.isEmpty() || ctx.getArgs().size() < 2) {
+ ModerationHelper.error(ctx, "Используй: `!swap <@user> <количество 1-5>`");
+ return;
+ }
+ var t = mentioned.get(0);
+ int swapCount;
+ try {
+ swapCount = Integer.parseInt(ctx.getArgs().get(1));
+ if (swapCount < 1 || swapCount > 5) throw new NumberFormatException();
+ } catch (NumberFormatException e) {
+ ModerationHelper.error(ctx, "Количество должно быть от 1 до 5");
+ return;
+ }
+ var voiceState = t.getVoiceState();
+ if (voiceState == null || !voiceState.inAudioChannel()) {
+ ModerationHelper.error(ctx, t.getUser().getName() + " не в голосовом канале");
+ return;
+ }
+ var original = voiceState.getChannel().asVoiceChannel();
+ var channels =
+ g.getVoiceChannels().stream().filter(vc -> !vc.getId().equals(original.getId())).toList();
+ if (channels.isEmpty()) {
+ ModerationHelper.error(ctx, "Недостаточно голосовых каналов для swap");
+ return;
+ }
+ ctx.getChannel()
+ .sendMessageEmbeds(
+ EmbedTemplates.success(
+ "🔄 Swap запущен!",
+ "Перекидываю " + t.getAsMention() + " между каналами " + swapCount + " раз(а)"))
+ .queue();
+ Thread.startVirtualThread(
+ () -> {
+ try {
+ for (int i = 0; i < swapCount; i++) {
+ var rnd = channels.get(random.nextInt(channels.size()));
+ g.moveVoiceMember(t, rnd).queue();
+ Thread.sleep(800);
+ g.moveVoiceMember(t, original).queue();
+ Thread.sleep(800);
+ }
+ } catch (Exception e) {
+ ModerationHelper.error(ctx, "Не удалось выполнить swap: " + e.getMessage());
+ }
+ });
+ }
+}
diff --git a/src/main/java/creator/loki/feature/moderation/commands/utils/SwapAndCaseCommand.java b/src/main/java/creator/loki/feature/moderation/commands/utils/SwapAndCaseCommand.java
deleted file mode 100644
index 649cdcd..0000000
--- a/src/main/java/creator/loki/feature/moderation/commands/utils/SwapAndCaseCommand.java
+++ /dev/null
@@ -1,189 +0,0 @@
-package creator.loki.feature.moderation.commands.utils;
-
-import java.awt.Color;
-import java.util.Random;
-
-import creator.loki.command.Command;
-import creator.loki.command.CommandContext;
-import creator.loki.embed.EmbedFactory;
-import creator.loki.embed.EmbedTemplates;
-import creator.loki.feature.moderation.repository.ModerationCaseRepository;
-import net.dv8tion.jda.api.Permission;
-
-public final class SwapAndCaseCommand {
- private SwapAndCaseCommand() {}
-
- public static Command createSwap() {
- return new Swap();
- }
-
- public static Command createCaseLookup(ModerationCaseRepository cr) {
- return new Case(cr);
- }
-
- private static class Swap implements Command {
- private final Random random = new Random();
-
- @Override
- public String getName() {
- return "swap";
- }
-
- @Override
- public String getDescription() {
- return "Перекидывает пользователя между случайными голосовыми каналами";
- }
-
- @Override
- public String getUsage() {
- return "swap <@user> <количество 1-5>";
- }
-
- @Override
- public String getCategory() {
- return "Модерация";
- }
-
- @Override
- public void execute(CommandContext ctx) {
- var g = ctx.getGuild();
- if (g == null) {
- err(ctx, "Команда доступна только на сервере");
- return;
- }
- var mod = ctx.getAuthor();
- if (mod == null || !mod.hasPermission(Permission.VOICE_MOVE_OTHERS)) {
- err(ctx, "Нет прав на перемещение");
- return;
- }
- if (!g.getMember(g.getJDA().getSelfUser()).hasPermission(Permission.VOICE_MOVE_OTHERS)) {
- err(ctx, "У бота нет прав на перемещение");
- return;
- }
- var mentioned = ctx.getMessage().getMentions().getMembers();
- if (mentioned.isEmpty() || ctx.getArgs().size() < 2) {
- err(ctx, "Используй: `!swap <@user> <количество 1-5>`");
- return;
- }
- var t = mentioned.get(0);
- int swapCount;
- try {
- swapCount = Integer.parseInt(ctx.getArgs().get(1));
- if (swapCount < 1 || swapCount > 5) throw new NumberFormatException();
- } catch (NumberFormatException e) {
- err(ctx, "Количество должно быть от 1 до 5");
- return;
- }
- var voiceState = t.getVoiceState();
- if (voiceState == null || !voiceState.inAudioChannel()) {
- err(ctx, t.getUser().getName() + " не в голосовом канале");
- return;
- }
- var original = voiceState.getChannel().asVoiceChannel();
- var channels =
- g.getVoiceChannels().stream().filter(vc -> !vc.getId().equals(original.getId())).toList();
- if (channels.isEmpty()) {
- err(ctx, "Недостаточно голосовых каналов для swap");
- return;
- }
- ctx.getChannel()
- .sendMessageEmbeds(
- EmbedTemplates.success(
- "🔄 Swap запущен!",
- "Перекидываю " + t.getAsMention() + " между каналами " + swapCount + " раз(а)"))
- .queue();
- Thread.startVirtualThread(
- () -> {
- try {
- for (int i = 0; i < swapCount; i++) {
- var rnd = channels.get(random.nextInt(channels.size()));
- g.moveVoiceMember(t, rnd).queue();
- Thread.sleep(800);
- g.moveVoiceMember(t, original).queue();
- Thread.sleep(800);
- }
- } catch (Exception e) {
- err(ctx, "Не удалось выполнить swap: " + e.getMessage());
- }
- });
- }
- }
-
- private static class Case implements Command {
- private final ModerationCaseRepository cases;
-
- Case(ModerationCaseRepository cr) {
- cases = cr;
- }
-
- @Override
- public String getName() {
- return "case";
- }
-
- @Override
- public String getDescription() {
- return "Показать кейс модерации по ID";
- }
-
- @Override
- public String getUsage() {
- return "case ";
- }
-
- @Override
- public String getCategory() {
- return "Модерация";
- }
-
- @Override
- public void execute(CommandContext ctx) {
- var g = ctx.getGuild();
- if (g == null) {
- err(ctx, "Команда только на сервере");
- return;
- }
- var mod = ctx.getAuthor();
- if (mod == null || !mod.hasPermission(Permission.KICK_MEMBERS)) {
- err(ctx, "Нужно **Kick Members** или выше");
- return;
- }
- if (ctx.getArgs().isEmpty()) {
- err(ctx, "Использование: `" + getUsage() + "`");
- return;
- }
- long caseId;
- try {
- caseId = Long.parseLong(ctx.getArgs().get(0).trim());
- } catch (NumberFormatException e) {
- err(ctx, "ID кейса должен быть числом");
- return;
- }
- cases
- .findById(g.getId(), caseId)
- .ifPresentOrElse(
- c ->
- ctx.getChannel()
- .sendMessageEmbeds(
- EmbedFactory.create()
- .withTitle("Кейс #" + c.id())
- .withColor(Color.BLUE)
- .addField("Действие", "`" + c.action() + "`", true)
- .addField("Цель", "<@" + c.targetId() + ">", true)
- .addField("Модератор", "<@" + c.moderatorId() + ">", true)
- .addField("Причина", trunc(c.reason(), 1000), false)
- .addField("Время", "", false)
- .build())
- .queue(),
- () -> err(ctx, "Кейса `" + caseId + "` на этом сервере нет"));
- }
- }
-
- private static String trunc(String s, int max) {
- return s.length() <= max ? s : s.substring(0, max - 1) + "…";
- }
-
- private static void err(CommandContext ctx, String msg) {
- ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
- }
-}
diff --git a/src/main/java/creator/loki/feature/moderation/commands/warn/WarnCommand.java b/src/main/java/creator/loki/feature/moderation/commands/warn/WarnCommand.java
index 436b076..56f6820 100644
--- a/src/main/java/creator/loki/feature/moderation/commands/warn/WarnCommand.java
+++ b/src/main/java/creator/loki/feature/moderation/commands/warn/WarnCommand.java
@@ -13,6 +13,7 @@ import creator.loki.feature.moderation.MuteScheduler;
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
import creator.loki.feature.moderation.repository.ModerationCaseRepository;
import creator.loki.feature.moderation.repository.WarningRepository;
+import creator.loki.feature.moderation.util.ModerationHelper;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Guild;
@@ -66,38 +67,46 @@ public class WarnCommand implements Command {
@Override
public void execute(CommandContext ctx) {
+ var t = validateInput(ctx);
+ if (t != null) saveWarning(ctx, t);
+ }
+
+ private Member validateInput(CommandContext ctx) {
var g = ctx.getGuild();
if (g == null) {
- err(ctx, "Только на сервере");
- return;
+ ModerationHelper.error(ctx, "Только на сервере");
+ return null;
}
var mod = ctx.getAuthor();
if (mod == null || !mod.hasPermission(Permission.KICK_MEMBERS)) {
- err(ctx, "Нужно **Kick Members**");
- return;
+ ModerationHelper.error(ctx, "Нужно **Kick Members**");
+ return null;
}
var mentioned = ctx.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
- err(ctx, "Укажи пользователя: `" + getUsage() + "`");
- return;
+ ModerationHelper.error(ctx, "Укажи пользователя: `" + getUsage() + "`");
+ return null;
}
var t = mentioned.get(0);
if (t.getId().equals(mod.getId())) {
- err(ctx, "Нельзя выдать варн самому себе");
- return;
+ ModerationHelper.error(ctx, "Нельзя выдать варн самому себе");
+ return null;
}
if (!mod.canInteract(t)) {
- err(ctx, "Участник выше ваших ролей");
- return;
+ ModerationHelper.error(ctx, "Участник выше ваших ролей");
+ return null;
}
- var reason =
- ctx.getArgs().size() > 1
- ? ctx.getArgs().stream().skip(1).collect(Collectors.joining(" ")).trim()
- : "Без причины";
+ return t;
+ }
+
+ private void saveWarning(CommandContext ctx, Member t) {
+ var g = ctx.getGuild();
+ var mod = ctx.getAuthor();
+ var reason = ctx.getArgs().stream().skip(1).collect(Collectors.joining(" ")).trim();
if (reason.isEmpty()) reason = "Без причины";
long id = warnings.insert(g.getId(), t.getId(), mod.getId(), reason);
if (id < 0) {
- err(ctx, "Не удалось сохранить предупреждение");
+ ModerationHelper.error(ctx, "Не удалось сохранить предупреждение");
return;
}
int total = warnings.countByUser(g.getId(), t.getId());
@@ -110,7 +119,7 @@ public class WarnCommand implements Command {
+ "\n**ID записи:** `"
+ id
+ "`\n**Причина:** "
- + trunc(reason, 500)
+ + ModerationHelper.trunc(reason, 500)
+ "\n**Всего варнов:** "
+ total))
.queue();
@@ -133,7 +142,7 @@ public class WarnCommand implements Command {
.addField("Модератор", mod.getAsMention(), false)
.addField("ID записи", "`" + warnId + "`", true)
.addField("Всего варнов", String.valueOf(total), true)
- .addField("Причина", trunc(reason, 1000), false)
+ .addField("Причина", ModerationHelper.trunc(reason, 1000), false)
.setTimestamp(OffsetDateTime.now())
.build())
.queue(s -> {}, e -> {});
@@ -148,7 +157,7 @@ public class WarnCommand implements Command {
}
var self = g.getMember(g.getJDA().getSelfUser());
if (!self.canInteract(t) || !self.canInteract(muteRole)) return;
- var reason = "Авто-мут: 3+ варна";
+ String reason = "Авто-мут: 3+ варна";
long untilMs = System.currentTimeMillis() + AUTO_MUTE_HOURS * 3600 * 1000;
long durSec = AUTO_MUTE_HOURS * 3600;
long caseId = cases.insert(g.getId(), "MUTE", t.getId(), mod.getId(), reason, durSec, untilMs);
@@ -173,12 +182,4 @@ public class WarnCommand implements Command {
},
err -> activeMutes.delete(g.getId(), t.getId()));
}
-
- private static String trunc(String s, int max) {
- return s.length() <= max ? s : s.substring(0, max - 1) + "…";
- }
-
- private static void err(CommandContext ctx, String msg) {
- ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
- }
}
diff --git a/src/main/java/creator/loki/feature/moderation/log/ModerationLogger.java b/src/main/java/creator/loki/feature/moderation/log/ModerationLogger.java
index 3f17bee..14b4d6d 100644
--- a/src/main/java/creator/loki/feature/moderation/log/ModerationLogger.java
+++ b/src/main/java/creator/loki/feature/moderation/log/ModerationLogger.java
@@ -12,7 +12,6 @@ import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.audit.ActionType;
import net.dv8tion.jda.api.audit.AuditLogEntry;
import net.dv8tion.jda.api.entities.Guild;
-import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.guild.GuildBanEvent;
import net.dv8tion.jda.api.events.guild.GuildUnbanEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
@@ -35,165 +34,118 @@ public class ModerationLogger extends ListenerAdapter {
}
private void handle(
- Guild guild, ActionType type, int limit, Function, EmbedBuilder> fn) {
- var log = getLogChannel(guild);
+ Guild g, ActionType t, int l, Function, EmbedBuilder> fn) {
+ var log = g.getTextChannelById(configRepo.get("moderation.log_channel_id"));
if (log == null) return;
- guild
- .retrieveAuditLogs()
- .type(type)
- .limit(limit)
- .queue(
- logs -> {
- var embed = fn.apply(logs);
- if (embed != null) {
- log.sendMessageEmbeds(embed.setTimestamp(OffsetDateTime.now()).build()).queue();
- }
- });
- }
-
- public void onMessageDelete(MessageDeleteEvent event) {
- var content = messageCache.getContent(event.getMessageId());
- handle(
- event.getGuild(),
- ActionType.MESSAGE_DELETE,
- 1,
+ var al = g.retrieveAuditLogs().type(t).limit(l);
+ al.queue(
logs -> {
- var entry = logs.isEmpty() ? null : logs.get(0);
- return LogEmbeds.messageDelete(entry, event.getChannel().getAsMention(), content);
+ var embed = fn.apply(logs);
+ if (embed == null) return;
+ log.sendMessageEmbeds(embed.setTimestamp(OffsetDateTime.now()).build()).queue();
});
}
- public void onMessageBulkDelete(MessageBulkDeleteEvent event) {
- handle(
- event.getGuild(),
- ActionType.MESSAGE_BULK_DELETE,
- 1,
- logs -> {
- var entry = logs.isEmpty() ? null : logs.get(0);
- return LogEmbeds.messageBulkDelete(
- entry, event.getChannel().getAsMention(), event.getMessageIds().size());
- });
+ public void onMessageDelete(MessageDeleteEvent e) {
+ var content = messageCache.getContent(e.getMessageId());
+ handle(e.getGuild(), ActionType.MESSAGE_DELETE, 1, l -> md(e, content, l));
}
- @Override
- public void onGuildBan(GuildBanEvent event) {
- handle(
- event.getGuild(),
- ActionType.BAN,
- 1,
- logs -> {
- if (logs.isEmpty()) return null;
- var entry = logs.get(0);
- var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно";
- var reason = entry.getReason() != null ? entry.getReason() : "Не указана";
- return LogEmbeds.ban(
- event.getUser().getAsMention(), mod, reason, event.getUser().getAvatarUrl());
- });
+ private EmbedBuilder md(MessageDeleteEvent e, String c, List l) {
+ var entry = l.isEmpty() ? null : l.get(0);
+ return LogEmbeds.messageDelete(entry, e.getChannel().getAsMention(), c);
}
- @Override
- public void onGuildUnban(GuildUnbanEvent event) {
- handle(
- event.getGuild(),
- ActionType.UNBAN,
- 1,
- logs -> {
- if (logs.isEmpty()) return null;
- var entry = logs.get(0);
- var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно";
- return LogEmbeds.unban(
- event.getUser().getAsMention(), mod, event.getUser().getAvatarUrl());
- });
+ public void onMessageBulkDelete(MessageBulkDeleteEvent e) {
+ handle(e.getGuild(), ActionType.MESSAGE_BULK_DELETE, 1, l -> mbd(e, l));
}
- @Override
- public void onGuildMemberRemove(GuildMemberRemoveEvent event) {
- handle(
- event.getGuild(),
- ActionType.KICK,
- 1,
- logs -> {
- if (logs.isEmpty()) return null;
- var entry = logs.get(0);
- if (!entry.getTargetId().equals(event.getUser().getId())) return null;
- var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно";
- var reason = entry.getReason() != null ? entry.getReason() : "Не указана";
- return LogEmbeds.kick(
- event.getUser().getAsMention(), mod, reason, event.getUser().getAvatarUrl());
- });
+ private EmbedBuilder mbd(MessageBulkDeleteEvent e, List l) {
+ var entry = l.isEmpty() ? null : l.get(0);
+ var size = e.getMessageIds().size();
+ return LogEmbeds.messageBulkDelete(entry, e.getChannel().getAsMention(), size);
}
- @Override
- public void onGuildMemberUpdateTimeOut(GuildMemberUpdateTimeOutEvent event) {
- var muted = event.getNewTimeOutEnd() != null;
- handle(
- event.getGuild(),
- ActionType.MEMBER_UPDATE,
- 5,
- logs -> {
- var entry =
- logs.stream()
- .filter(e -> event.getUser().getId().equals(e.getTargetId()))
- .findFirst()
- .orElse(null);
- if (entry == null) return null;
- var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно";
- var end = muted ? event.getNewTimeOutEnd().toEpochSecond() : null;
- return LogEmbeds.timeout(
- event.getUser().getAsMention(), mod, muted, end, entry.getReason());
- });
+ public void onGuildBan(GuildBanEvent ev) {
+ handle(ev.getGuild(), ActionType.BAN, 1, l -> b(ev, l));
}
- @Override
- public void onGuildVoiceGuildMute(GuildVoiceGuildMuteEvent event) {
- var muted = event.isGuildMuted();
- handle(
- event.getGuild(),
- ActionType.MEMBER_UPDATE,
- 5,
- logs -> {
- var entry =
- logs.stream()
- .filter(e -> event.getMember().getId().equals(e.getTargetId()))
- .filter(e -> e.getChangeByKey("mute") != null)
- .findFirst()
- .orElse(null);
- var mod =
- entry != null && entry.getUser() != null ? entry.getUser().getAsMention() : null;
- return LogEmbeds.voiceMute(
- event.getMember().getAsMention(),
- event.getVoiceState().getChannel().getAsMention(),
- muted,
- mod);
- });
+ private EmbedBuilder b(GuildBanEvent ev, List l) {
+ if (l.isEmpty()) return null;
+ var user = ev.getUser();
+ var e = l.get(0);
+ var mod = e.getUser() != null ? e.getUser().getAsMention() : "Неизвестно";
+ var reason = e.getReason() != null ? e.getReason() : "Не указана";
+ return LogEmbeds.ban(user.getAsMention(), mod, reason, user.getAvatarUrl());
}
- @Override
- public void onGuildVoiceGuildDeafen(GuildVoiceGuildDeafenEvent event) {
- var deaf = event.isGuildDeafened();
- handle(
- event.getGuild(),
- ActionType.MEMBER_UPDATE,
- 5,
- logs -> {
- var entry =
- logs.stream()
- .filter(e -> event.getMember().getId().equals(e.getTargetId()))
- .filter(e -> e.getChangeByKey("deaf") != null)
- .findFirst()
- .orElse(null);
- var mod =
- entry != null && entry.getUser() != null ? entry.getUser().getAsMention() : null;
- return LogEmbeds.voiceDeafen(
- event.getMember().getAsMention(),
- event.getVoiceState().getChannel().getAsMention(),
- deaf,
- mod);
- });
+ public void onGuildUnban(GuildUnbanEvent ev) {
+ handle(ev.getGuild(), ActionType.UNBAN, 1, l -> ub(ev, l));
}
- private TextChannel getLogChannel(Guild guild) {
- var logChannelId = configRepo.get("moderation.log_channel_id");
- return logChannelId != null ? guild.getTextChannelById(logChannelId) : null;
+ private EmbedBuilder ub(GuildUnbanEvent ev, List l) {
+ if (l.isEmpty()) return null;
+ var user = ev.getUser();
+ var e = l.get(0);
+ var mod = e.getUser() != null ? e.getUser().getAsMention() : "Неизвестно";
+ return LogEmbeds.unban(user.getAsMention(), mod, user.getAvatarUrl());
+ }
+
+ public void onGuildMemberRemove(GuildMemberRemoveEvent ev) {
+ handle(ev.getGuild(), ActionType.KICK, 1, l -> k(ev, l));
+ }
+
+ private EmbedBuilder k(GuildMemberRemoveEvent ev, List l) {
+ if (l.isEmpty()) return null;
+ var e = l.get(0);
+ if (!e.getTargetId().equals(ev.getUser().getId())) return null;
+ var mod = e.getUser() != null ? e.getUser().getAsMention() : "Неизвестно";
+ var reason = e.getReason() != null ? e.getReason() : "Не указана";
+ return LogEmbeds.kick(ev.getUser().getAsMention(), mod, reason, ev.getUser().getAvatarUrl());
+ }
+
+ public void onGuildMemberUpdateTimeOut(GuildMemberUpdateTimeOutEvent e) {
+ var muted = e.getNewTimeOutEnd() != null;
+ handle(e.getGuild(), ActionType.MEMBER_UPDATE, 5, l -> t(e, muted, l));
+ }
+
+ private EmbedBuilder t(GuildMemberUpdateTimeOutEvent e, boolean muted, List l) {
+ var entry = findEntry(l, e.getUser().getId());
+ if (entry == null) return null;
+ var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно";
+ var end = muted ? e.getNewTimeOutEnd().toEpochSecond() : null;
+ return LogEmbeds.timeout(e.getUser().getAsMention(), mod, muted, end, entry.getReason());
+ }
+
+ public void onGuildVoiceGuildMute(GuildVoiceGuildMuteEvent e) {
+ var muted = e.isGuildMuted();
+ handle(e.getGuild(), ActionType.MEMBER_UPDATE, 5, l -> vm(e, muted, l));
+ }
+
+ private EmbedBuilder vm(GuildVoiceGuildMuteEvent e, boolean muted, List l) {
+ var member = e.getMember();
+ var ch = e.getVoiceState().getChannel();
+ var entry = findEntry(l, member.getId(), "mute");
+ var mod = entry != null && entry.getUser() != null ? entry.getUser().getAsMention() : null;
+ return LogEmbeds.voiceMute(member.getAsMention(), ch.getAsMention(), muted, mod);
+ }
+
+ public void onGuildVoiceGuildDeafen(GuildVoiceGuildDeafenEvent e) {
+ var deaf = e.isGuildDeafened();
+ handle(e.getGuild(), ActionType.MEMBER_UPDATE, 5, l -> vd(e, deaf, l));
+ }
+
+ private EmbedBuilder vd(GuildVoiceGuildDeafenEvent e, boolean deaf, List l) {
+ var member = e.getMember();
+ var ch = e.getVoiceState().getChannel();
+ var entry = findEntry(l, member.getId(), "deaf");
+ var mod = entry != null && entry.getUser() != null ? entry.getUser().getAsMention() : null;
+ return LogEmbeds.voiceDeafen(member.getAsMention(), ch.getAsMention(), deaf, mod);
+ }
+
+ private AuditLogEntry findEntry(List l, String targetId, String... key) {
+ var s = l.stream().filter(e -> targetId.equals(e.getTargetId()));
+ if (key.length > 0) s = s.filter(e -> e.getChangeByKey(key[0]) != null);
+ return s.findFirst().orElse(null);
}
}
diff --git a/src/main/java/creator/loki/feature/moderation/util/ModerationHelper.java b/src/main/java/creator/loki/feature/moderation/util/ModerationHelper.java
new file mode 100644
index 0000000..5cc17a3
--- /dev/null
+++ b/src/main/java/creator/loki/feature/moderation/util/ModerationHelper.java
@@ -0,0 +1,16 @@
+package creator.loki.feature.moderation.util;
+
+import creator.loki.command.CommandContext;
+import creator.loki.embed.EmbedTemplates;
+
+public final class ModerationHelper {
+ private ModerationHelper() {}
+
+ public static String trunc(String s, int max) {
+ return s.length() <= max ? s : s.substring(0, max - 1) + "…";
+ }
+
+ public static void error(CommandContext ctx, String msg) {
+ ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
+ }
+}
diff --git a/src/main/java/creator/loki/feature/music/commands/FilterCommands.java b/src/main/java/creator/loki/feature/music/commands/FilterCommands.java
index 88d1ee0..4d035a0 100644
--- a/src/main/java/creator/loki/feature/music/commands/FilterCommands.java
+++ b/src/main/java/creator/loki/feature/music/commands/FilterCommands.java
@@ -161,7 +161,10 @@ public class FilterCommands {
public void execute(CommandContext ctx) {
var m = mgr(pm, ctx);
if (!checkChannel(m, ctx)) return;
- boolean enabled = isNight ? !m.getScheduler().isNightcore() : !m.getScheduler().isVaporwave();
+ boolean enabled =
+ isNight
+ ? !m.getScheduler().filterState.isNightcore()
+ : !m.getScheduler().filterState.isVaporwave();
if (isNight) m.getScheduler().setNightcore(enabled);
else m.getScheduler().setVaporwave(enabled);
ctx.getChannel()
diff --git a/src/main/java/creator/loki/feature/music/commands/QueueCommands.java b/src/main/java/creator/loki/feature/music/commands/QueueCommands.java
index a8b4d7f..0e08eb6 100644
--- a/src/main/java/creator/loki/feature/music/commands/QueueCommands.java
+++ b/src/main/java/creator/loki/feature/music/commands/QueueCommands.java
@@ -1,207 +1,149 @@
package creator.loki.feature.music.commands;
+import static creator.loki.embed.EmbedTemplates.error;
+import static creator.loki.embed.EmbedTemplates.info;
+
import java.awt.Color;
-import java.util.List;
+import java.util.function.BiConsumer;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
-import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.music.core.PlayerManager;
-import dev.arbjerg.lavalink.client.player.Track;
+import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class QueueCommands {
+ private static final Color C = new Color(88, 101, 242);
+ private static final Logger LOG = LoggerFactory.getLogger(QueueCommands.class);
+
private QueueCommands() {}
public static Command queue(PlayerManager pm) {
- return new Queue(pm);
+ return new Mc(pm, "queue", "Показать очередь треков", null, null, QueueCommands::executeQueue);
}
public static Command nowplaying(PlayerManager pm) {
- return new NowPlaying(pm);
+ BiConsumer exec = QueueCommands::executeNowPlaying;
+ return new Mc(pm, "nowplaying", "Показать текущий трек", "nowplaying", "Музыка", exec);
}
public static Command volume(PlayerManager pm) {
- return new Volume(pm);
+ BiConsumer exec = QueueCommands::executeVolume;
+ return new Mc(pm, "volume", "Регулировка громкости (0-100)", "volume [0-100]", "Музыка", exec);
}
- private static final class Queue implements Command {
- private final PlayerManager pm;
+ private static final class Mc implements Command {
+ private final PlayerManager p;
+ private final String n, d, u, c;
+ private final BiConsumer e;
- Queue(PlayerManager pm) {
- this.pm = pm;
+ Mc(
+ PlayerManager p,
+ String n,
+ String d,
+ String u,
+ String c,
+ BiConsumer e) {
+ this.p = p;
+ this.n = n;
+ this.d = d;
+ this.u = u;
+ this.c = c;
+ this.e = e;
}
- @Override
public String getName() {
- return "queue";
+ return n;
}
- @Override
public String getDescription() {
- return "Показать очередь треков";
+ return d;
}
- @Override
- public void execute(CommandContext ctx) {
- var m = pm.getMusicManager(ctx.getGuild().getIdLong());
- Track cur = m.getScheduler().getCurrentTrack();
- List