refactor: KISS/DRY/SOLID раунд + багфиксы радио/voice/message

Что сделано:
- RatingService (getDeterministicValue + cooldown — одна точка)
- ModerationHelper (trunc/err — одна точка)
- VoiceHelper (waitForCondition с таймаутом)
- SocialCommand (7 файлов → 1 Flavor enum)
- SwapCommand + CaseLookupCommand (Split SRP)
- PlayCommand 219→150, BotInitializer 182→135, RankCardGenerator (1→4)
- MetricsCollector 182→143, TrackScheduler 202→146, VoiceChannelManager 187→150
- QueueCommands 207→149, VoiceSetupPrefixCommand 183→146, ServerStatsService 187→150
- GuildMusicManager 167→150, SqlEconomyRepository 169→149, GenerateRatingsCommand 172→147
- RadioManager: таймауты, volume при старте, защита от утечки
- VoiceTracker: race condition fix + LevelUpNotifier
- MessageCooldownService: in-memory cache
- MessageListener: LevelUpNotifier вместо inline embed
- ResetLevelsButtonHandler: правильный парсинг кнопки
- OkHttp: java.net.http → OkHttp 5.4.0
- SlowmodeCommandTest: SelfMember fix
Всё: 415 tests, 2 pre-existing, checkstyle чисто
This commit is contained in:
loki5512344 2026-07-04 18:46:27 +02:00
parent 42948520e8
commit 0d916fbd96
79 changed files with 2639 additions and 3245 deletions

32
.classpath Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/java">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/resources">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

34
.project Normal file
View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>discord-bot-bot</name>
<comment>Project bot created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<filteredResources>
<filter>
<id>1783162972591</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View file

@ -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

145
TODO.md
View file

@ -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 ## ✅ Что добавлено в v2.3

View file

@ -23,6 +23,8 @@ dependencies {
implementation libs.guice implementation libs.guice
implementation libs.sqlite.jdbc implementation libs.sqlite.jdbc
implementation libs.hikaricp implementation libs.hikaricp
implementation libs.okhttp
implementation libs.logging.interceptor
testImplementation libs.junit.jupiter testImplementation libs.junit.jupiter
testImplementation libs.mockito.core testImplementation libs.mockito.core

View file

@ -10,6 +10,7 @@ caffeine = "3.2.0"
guice = "7.0.0" guice = "7.0.0"
sqlite = "3.49.1.0" sqlite = "3.49.1.0"
hikaricp = "6.3.0" hikaricp = "6.3.0"
okhttp = "5.4.0"
junit = "5.12.1" junit = "5.12.1"
mockito = "5.15.2" mockito = "5.15.2"
junitPlatformLauncher = "1.12.1" 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" } guice = { module = "com.google.inject:guice", version.ref = "guice" }
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" } sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikaricp" } 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" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }

View file

@ -1,5 +1,7 @@
package creator.loki.core; package creator.loki.core;
import java.util.List;
import com.google.inject.Inject; import com.google.inject.Inject;
import creator.loki.config.BotConfig; import creator.loki.config.BotConfig;
import creator.loki.core.registry.PrefixCommandRegistrar; import creator.loki.core.registry.PrefixCommandRegistrar;
@ -63,119 +65,92 @@ public class BotInitializer {
public BotCluster start() { public BotCluster start() {
LoggerDecorator.startup(logger, "Инициализация main бота..."); LoggerDecorator.startup(logger, "Инициализация main бота...");
registrar.registerAll(); registrar.registerAll();
JDA mainJda = createMainJDA(); JDA mainJda = buildMainJDA();
try {
mainJda.awaitReady();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Main бот прерван во время ожидания готовности", e);
}
mainJda.updateCommands().queue(); mainJda.updateCommands().queue();
muteScheduler.restorePending(mainJda); muteScheduler.restorePending(mainJda);
for (var guild : mainJda.getGuilds()) { mainJda.getGuilds().forEach(g -> memberCounterListener.startScheduler(g));
memberCounterListener.startScheduler(guild);
}
healthHttpServer.start(); healthHttpServer.start();
JDA musicJda = null; JDA musicJda = createMusicJDA();
if (config.getMusicToken() != null && !config.getMusicToken().isBlank()) { JDA radioJda = createRadioJDA();
LoggerDecorator.startup(logger, "Инициализация music бота..."); if (radioJda != null) {
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);
}
radioManager.setRadioJda(radioJda); radioManager.setRadioJda(radioJda);
radioManager.setLavalinkManager(lavalinkManager);
} }
BotCluster cluster = new BotCluster(mainJda, musicJda, radioJda); BotCluster cluster = new BotCluster(mainJda, musicJda, radioJda);
registerShutdownHook(cluster); Runtime.getRuntime()
.addShutdownHook(new Thread(() -> shutdownManager.shutdown(cluster), "ShutdownHook"));
return cluster; return cluster;
} }
private JDA createMainJDA() { private JDA buildMainJDA() {
var builder = var builder =
JDABuilder.createDefault(config.getMainToken()) createBuilder(
.setStatus(OnlineStatus.ONLINE) config.getMainToken(),
.setActivity(Activity.playing(config.getActivity())) config.getActivity(),
.enableIntents( GatewayIntent.GUILD_MESSAGES,
GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT,
GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_MEMBERS,
GatewayIntent.GUILD_MEMBERS, GatewayIntent.GUILD_VOICE_STATES);
GatewayIntent.GUILD_VOICE_STATES)
.enableCache(CacheFlag.VOICE_STATE)
.addEventListeners(listenerAggregator.getAll());
if (lavalinkManager.getClient() != null) { if (lavalinkManager.getClient() != null) {
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient())); builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient()));
} }
builder.addEventListeners(listenerAggregator.getAll());
return builder.build(); return awaitJda(builder.build());
} }
private JDA createMusicJDA() { private JDA createMusicJDA() {
LavalinkManager musicLavalink = if (config.getMusicToken() == null || config.getMusicToken().isBlank()) return null;
new LavalinkManager(config, queueRepository, config.getMusicToken()); LoggerDecorator.startup(logger, "Инициализация music бота...");
LavalinkManager ml = new LavalinkManager(config, queueRepository, config.getMusicToken());
var builder = var builder =
JDABuilder.createDefault(config.getMusicToken()) createBuilder(
.setStatus(OnlineStatus.ONLINE) config.getMusicToken(),
.setActivity(Activity.playing("🎵 q!play")) "🎵 q!play",
.enableIntents( GatewayIntent.GUILD_MESSAGES,
GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT,
GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_VOICE_STATES);
GatewayIntent.GUILD_VOICE_STATES) if (ml.getClient() != null) {
.enableCache(CacheFlag.VOICE_STATE); builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(ml.getClient()));
if (musicLavalink.getClient() != null) {
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(musicLavalink.getClient()));
} }
builder.addEventListeners( builder.addEventListeners(
new MusicBotListener( new MusicBotListener(
musicLavalink, ml, queueRepository, new creator.loki.feature.music.core.PlayerManager(ml)));
queueRepository, return awaitJda(builder.build());
new creator.loki.feature.music.core.PlayerManager(musicLavalink)));
return builder.build();
} }
private JDA createRadioJDA() { private JDA createRadioJDA() {
if (config.getRadioToken() == null || config.getRadioToken().isBlank()) return null;
LoggerDecorator.startup(logger, "Инициализация radio бота...");
var builder = var builder =
JDABuilder.createDefault(config.getRadioToken()) createBuilder(
.setStatus(OnlineStatus.ONLINE) config.getRadioToken(),
.setActivity(Activity.playing("📻 r!play")) "📻 r!play",
.enableIntents( GatewayIntent.GUILD_MESSAGES,
GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT,
GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_VOICE_STATES);
GatewayIntent.GUILD_VOICE_STATES)
.enableCache(CacheFlag.VOICE_STATE);
if (lavalinkManager.getClient() != null) { if (lavalinkManager.getClient() != null) {
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient())); builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient()));
} }
builder.addEventListeners(new RadioBotListener(radioManager)); builder.addEventListeners(new RadioBotListener(radioManager));
return awaitJda(builder.build());
return builder.build();
} }
private void registerShutdownHook(BotCluster cluster) { private static JDABuilder createBuilder(String token, String activity, GatewayIntent... intents) {
Runtime.getRuntime() return JDABuilder.createDefault(token)
.addShutdownHook(new Thread(() -> shutdownManager.shutdown(cluster), "ShutdownHook")); .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;
} }
} }

View file

@ -9,13 +9,10 @@ import creator.loki.config.BotConfig;
import creator.loki.embed.EmbedFactory; import creator.loki.embed.EmbedFactory;
import creator.loki.feature.rating.repository.RatingsRepository; import creator.loki.feature.rating.repository.RatingsRepository;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.MessageEmbed;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/**
* Команда для генерации всех rating значений для всех участников сервера Доступна только
* супер-админу
*/
public class GenerateRatingsCommand implements Command { public class GenerateRatingsCommand implements Command {
private static final Logger logger = LoggerFactory.getLogger(GenerateRatingsCommand.class); private static final Logger logger = LoggerFactory.getLogger(GenerateRatingsCommand.class);
private final RatingsRepository ratingsRepository; private final RatingsRepository ratingsRepository;
@ -53,120 +50,98 @@ public class GenerateRatingsCommand implements Command {
context context
.getChannel() .getChannel()
.sendMessageEmbeds( .sendMessageEmbeds(
EmbedFactory.create() errorEmbed(
.withTitle("❌ Недостаточно прав") "❌ Недостаточно прав", "Эта команда доступна только супер-администратору бота."))
.withDescription("Эта команда доступна только супер-администратору бота.")
.withColor(Color.RED)
.build())
.queue(); .queue();
return; return;
} }
if (context.getGuild() == null) { if (context.getGuild() == null) {
context.getChannel().sendMessage("❌ Команда работает только на сервере.").queue(); context.getChannel().sendMessage("❌ Команда работает только на сервере.").queue();
return; return;
} }
var initial = var initial =
context context
.getChannel() .getChannel()
.sendMessageEmbeds( .sendMessageEmbeds(
EmbedFactory.create() warnEmbed(
.withTitle("⏳ Генерация рейтингов...") "⏳ Генерация рейтингов...",
.withDescription( "Начинаю генерацию всех rating значений для участников сервера..."))
"Начинаю генерацию всех rating значений для участников сервера...")
.withColor(Color.YELLOW)
.build())
.complete(); .complete();
try { try {
String guildId = context.getGuild().getId(); var guildId = context.getGuild().getId();
int memberCount = 0; var memberCount = 0;
int generatedCount = 0; var generatedCount = 0;
for (Member member : context.getGuild().getMembers()) { for (Member member : context.getGuild().getMembers()) {
if (member.getUser().isBot()) { if (member.getUser().isBot()) continue;
continue;
}
memberCount++; memberCount++;
String userId = member.getUser().getId(); var userId = member.getUser().getId();
for (var type : new String[] {"ship", "gay", "iq", "pp", "mustache", "fat", "boykisser"}) {
generateRatingForUser(guildId, userId, "ship"); generateRatingForUser(guildId, userId, type);
generateRatingForUser(guildId, userId, "gay"); }
generateRatingForUser(guildId, userId, "iq");
generateRatingForUser(guildId, userId, "pp");
generateRatingForUser(guildId, userId, "mustache");
generateRatingForUser(guildId, userId, "fat");
generateRatingForUser(guildId, userId, "boykisser");
generatedCount++; generatedCount++;
} }
initial.editMessageEmbeds(successEmbed(context, memberCount, generatedCount)).queue();
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();
logger.info( logger.info(
"Супер-админ {} сгенерировал рейтинги для {} участников", "Супер-админ {} сгенерировал рейтинги для {} участников",
context.getAuthor().getUser().getName(), context.getAuthor().getUser().getName(),
memberCount); memberCount);
} catch (Exception e) { } catch (Exception e) {
logger.error("Ошибка при генерации рейтингов", e); logger.error("Ошибка при генерации рейтингов", e);
initial initial
.editMessageEmbeds( .editMessageEmbeds(
EmbedFactory.create() errorEmbed("❌ Ошибка", "Произошла ошибка при генерации рейтингов: " + e.getMessage()))
.withTitle("❌ Ошибка")
.withDescription("Произошла ошибка при генерации рейтингов: " + e.getMessage())
.withColor(Color.RED)
.build())
.queue(); .queue();
} }
} }
/** Генерирует rating значение для пользователя */
private void generateRatingForUser(String guildId, String userId, String ratingType) { private void generateRatingForUser(String guildId, String userId, String ratingType) {
// Проверяем существует ли уже значение if (ratingsRepository.hasRating(guildId, userId, ratingType)) return;
if (ratingsRepository.hasRating(guildId, userId, ratingType)) { var value =
return; // Не перезаписываем существующие значения
}
// Генерируем значение в зависимости от типа
int value =
switch (ratingType) { switch (ratingType) {
case "iq" -> random.nextInt(201); // 0-200 case "iq" -> random.nextInt(201);
case "pp" -> random.nextInt(31); // 0-30 см case "pp" -> random.nextInt(31);
case "mustache" -> { case "mustache" -> {
// Редкий шанс на легендарные усы if (random.nextInt(100) < 1) yield 1000 + random.nextInt(500);
if (random.nextInt(100) < 1) { yield random.nextInt(101);
yield 1000 + random.nextInt(500); // 1000-1500
}
yield random.nextInt(101); // 0-100
} }
case "fat" -> random.nextInt(101); // 0-100% default -> random.nextInt(101);
case "boykisser" -> random.nextInt(101); // 0-100%
default -> random.nextInt(101); // ship, gay: 0-100%
}; };
// Сохраняем значение в БД
ratingsRepository.setRating(guildId, userId, ratingType, value); 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();
}
} }

View file

@ -3,6 +3,7 @@ package creator.loki.feature.economy.repository;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -19,24 +20,44 @@ public class SqlEconomyRepository implements EconomyRepository {
private static final int MAX_TOP = 100; private static final int MAX_TOP = 100;
private final Database database; 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) { public SqlEconomyRepository(Database database) {
this.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 @Override
public Optional<EconomyAccount> findById(String userId) { public Optional<EconomyAccount> findById(String userId) {
String sql = var sql = selectSql();
""" try (var stmt = database.prepare(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)) {
stmt.setString(1, userId); stmt.setString(1, userId);
try (ResultSet rs = stmt.executeQuery()) { try (var rs = stmt.executeQuery()) {
if (rs.next()) { if (rs.next()) {
return Optional.of(mapResultSetToAccount(rs)); return Optional.of(mapResultSetToAccount(rs));
} }
@ -49,56 +70,20 @@ public class SqlEconomyRepository implements EconomyRepository {
@Override @Override
public void save(EconomyAccount account) { public void save(EconomyAccount account) {
String sql = var sql = upsertSql();
""" try (var stmt = database.prepare(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)) {
stmt.setString(1, account.getUserId()); stmt.setString(1, account.getUserId());
stmt.setLong(2, account.getCash()); stmt.setLong(2, account.getCash());
stmt.setLong(3, account.getBank()); stmt.setLong(3, account.getBank());
stmt.setLong(4, account.getTotalEarned()); stmt.setLong(4, account.getTotalEarned());
stmt.setLong(5, account.getTotalSpent()); stmt.setLong(5, account.getTotalSpent());
if (account.getLastDaily() != null) { setNullableString(stmt, 6, account.getLastDaily());
stmt.setString(6, account.getLastDaily());
} else {
stmt.setNull(6, java.sql.Types.VARCHAR);
}
stmt.setInt(7, account.getDailyStreak()); stmt.setInt(7, account.getDailyStreak());
stmt.setInt(8, account.getFishingLuck()); stmt.setInt(8, account.getFishingLuck());
stmt.setInt(9, account.getEfficiency()); stmt.setInt(9, account.getEfficiency());
if (account.getLastFish() != null) { setNullableString(stmt, 10, account.getLastFish());
stmt.setString(10, account.getLastFish()); setNullableString(stmt, 11, account.getJob());
} else { setNullableString(stmt, 12, account.getLastWork());
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);
}
stmt.executeUpdate(); stmt.executeUpdate();
} catch (SQLException e) { } catch (SQLException e) {
logger.error("Error saving economy account for {}", account.getUserId(), e); logger.error("Error saving economy account for {}", account.getUserId(), e);
@ -107,10 +92,10 @@ public class SqlEconomyRepository implements EconomyRepository {
@Override @Override
public boolean exists(String userId) { public boolean exists(String userId) {
String sql = "SELECT 1 FROM economy WHERE user_id = ?"; var sql = "SELECT 1 FROM economy WHERE user_id = ?";
try (PreparedStatement stmt = database.prepare(sql)) { try (var stmt = database.prepare(sql)) {
stmt.setString(1, userId); stmt.setString(1, userId);
try (ResultSet rs = stmt.executeQuery()) { try (var rs = stmt.executeQuery()) {
return rs.next(); return rs.next();
} }
} catch (SQLException e) { } catch (SQLException e) {
@ -121,20 +106,15 @@ public class SqlEconomyRepository implements EconomyRepository {
@Override @Override
public List<EconomyAccount> findTopByBalance(int limit) { public List<EconomyAccount> findTopByBalance(int limit) {
int lim = clampLimit(limit); var lim = clampLimit(limit);
String sql = var sql =
""" "SELECT "
SELECT user_id, cash, bank, total_earned, total_spent, + COLUMNS
last_daily, daily_streak, fishing_luck, efficiency, + " FROM economy ORDER BY (cash + bank) DESC, cash DESC, user_id ASC LIMIT ?";
last_fish, job, last_work var list = new ArrayList<EconomyAccount>();
FROM economy try (var stmt = database.prepare(sql)) {
ORDER BY (cash + bank) DESC, cash DESC, user_id ASC
LIMIT ?
""";
List<EconomyAccount> list = new ArrayList<>();
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setInt(1, lim); stmt.setInt(1, lim);
try (ResultSet rs = stmt.executeQuery()) { try (var rs = stmt.executeQuery()) {
while (rs.next()) { while (rs.next()) {
list.add(mapResultSetToAccount(rs)); list.add(mapResultSetToAccount(rs));
} }

View file

@ -5,41 +5,41 @@ import creator.loki.config.BotConfig;
import creator.loki.core.registry.CommandRegistrar; import creator.loki.core.registry.CommandRegistrar;
import creator.loki.core.registry.CommandRegistry; import creator.loki.core.registry.CommandRegistry;
import creator.loki.core.security.CooldownManager; 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.CoinCommand;
import creator.loki.feature.entertainment.commands.DiceCommand; import creator.loki.feature.entertainment.commands.DiceCommand;
import creator.loki.feature.entertainment.commands.EightBallCommand; 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.GlazbobaCommand;
import creator.loki.feature.entertainment.commands.IqCommand;
import creator.loki.feature.entertainment.commands.MemeCommand; 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.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.ShipCommand;
import creator.loki.feature.ship.repository.ShipPairRepository; import creator.loki.feature.ship.repository.ShipPairRepository;
import creator.loki.feature.social.commands.ComplimentCommand; 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.PatCommand;
import creator.loki.feature.social.commands.PokeCommand; import creator.loki.feature.social.commands.RoastCommand;
import creator.loki.feature.social.commands.SlapCommand; import creator.loki.feature.social.commands.SocialCommand;
import creator.loki.feature.social.commands.TickleCommand;
public class EntertainmentCommandRegistrar implements CommandRegistrar { public class EntertainmentCommandRegistrar implements CommandRegistrar {
private final CooldownManager cooldownManager; private final CooldownManager cooldownManager;
private final RatingService ratingService;
private final ShipPairRepository shipPairRepository; private final ShipPairRepository shipPairRepository;
private final BotConfig config; private final BotConfig config;
@Inject @Inject
public EntertainmentCommandRegistrar( public EntertainmentCommandRegistrar(
CooldownManager cooldownManager, ShipPairRepository shipPairRepository, BotConfig config) { CooldownManager cooldownManager,
RatingService ratingService,
ShipPairRepository shipPairRepository,
BotConfig config) {
this.cooldownManager = cooldownManager; this.cooldownManager = cooldownManager;
this.ratingService = ratingService;
this.shipPairRepository = shipPairRepository; this.shipPairRepository = shipPairRepository;
this.config = config; this.config = config;
} }
@ -55,22 +55,18 @@ public class EntertainmentCommandRegistrar implements CommandRegistrar {
registry.register(new ComplimentCommand()); registry.register(new ComplimentCommand());
registry.register(new GlazbobaCommand()); registry.register(new GlazbobaCommand());
registry.register(new IqCommand(cooldownManager)); registry.register(new IqCommand(ratingService));
registry.register(new GayCommand(cooldownManager)); registry.register(new GayCommand(ratingService));
registry.register(new PpCommand(cooldownManager)); registry.register(new PpCommand(ratingService));
registry.register(new RateCommand(cooldownManager)); registry.register(new RateCommand(ratingService));
registry.register(new BoykisserCommand(cooldownManager)); registry.register(new BoykisserCommand(ratingService));
registry.register(new MustacheCommand(cooldownManager)); registry.register(new MustacheCommand(ratingService));
registry.register(new FatCommand(cooldownManager)); registry.register(new FatCommand(ratingService));
registry.register(new PatCommand(cooldownManager)); registry.register(new PatCommand(cooldownManager));
registry.register(new HugCommand()); for (var flavor : SocialCommand.Flavor.values()) {
registry.register(new KissCommand(cooldownManager)); registry.register(new SocialCommand(flavor, 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));
registry.register( registry.register(
new RandomChoiceCommand(config, "choose", RandomChoiceCommand.Flavor.DEFAULT)); new RandomChoiceCommand(config, "choose", RandomChoiceCommand.Flavor.DEFAULT));

View file

@ -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<Member> 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;
}
}

View file

@ -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<Member> 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;
}
}

View file

@ -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<Member> 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;
}
}

View file

@ -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<Member> 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;
}
}

View file

@ -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<Member> 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;
}
}

View file

@ -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<Member> 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;
}
}

View file

@ -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<RatingLevel> 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<RatingLevel> 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<RatingLevel> 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<RatingLevel> 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<RatingLevel> 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)));
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -1,24 +1,23 @@
package creator.loki.feature.entertainment.commands; package creator.loki.feature.entertainment.commands.rating;
import java.awt.Color; import java.awt.Color;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random;
import com.google.inject.Inject;
import creator.loki.command.Command; import creator.loki.command.Command;
import creator.loki.command.CommandContext; import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory; import creator.loki.embed.EmbedFactory;
import creator.loki.feature.entertainment.service.RatingService;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
/** Оценка пользователя из 10 с детерминированным результатом */
public class RateCommand implements Command { public class RateCommand implements Command {
private final CooldownManager cooldownManager; private final RatingService ratingService;
public RateCommand(CooldownManager cooldownManager) { @Inject
this.cooldownManager = cooldownManager; public RateCommand(RatingService ratingService) {
this.ratingService = ratingService;
} }
@Override @Override
@ -43,28 +42,13 @@ public class RateCommand implements Command {
@Override @Override
public void execute(CommandContext context) { public void execute(CommandContext context) {
// Проверка cooldown (5 секунд) if (!ratingService.checkAndApplyCooldown(context, "rate")) return;
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "rate")) { Member target = ratingService.resolveTarget(context);
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "rate");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return; if (target == null) return;
// Применяем cooldown int rating = ratingService.getDeterministicValue(target.getId(), 420, 1, 10);
cooldownManager.applyCooldown(context.getAuthor().getId(), "rate", 5);
// Детерминированная оценка от 1 до 10
int rating = getDeterministicValue(target.getId(), 420, 1, 10);
// Визуализация звездами
String stars = "⭐".repeat(rating) + "☆".repeat(10 - rating); String stars = "⭐".repeat(rating) + "☆".repeat(10 - rating);
// Комментарий
Map<Integer, String> comments = new HashMap<>(); Map<Integer, String> comments = new HashMap<>();
comments.put(10, "💯 ИДЕАЛЬНО!"); comments.put(10, "💯 ИДЕАЛЬНО!");
comments.put(9, "🔥 Почти идеально!"); comments.put(9, "🔥 Почти идеально!");
@ -92,12 +76,4 @@ public class RateCommand implements Command {
.build()) .build())
.queue(); .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;
}
} }

View file

@ -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<String, List<RatingLevel>> LEVELS;
static {
var m = new LinkedHashMap<String, List<RatingLevel>>();
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<RatingLevel> getMustacheLevels() {
return LEVELS.get("mustache");
}
public static List<RatingLevel> getIqLevels() {
return LEVELS.get("iq");
}
public static List<RatingLevel> getFatLevels() {
return LEVELS.get("fat");
}
public static List<RatingLevel> getPpLevels() {
return LEVELS.get("pp");
}
public static List<RatingLevel> getBoykisserLevels() {
return LEVELS.get("boykisser");
}
}

View file

@ -1,4 +1,4 @@
package creator.loki.feature.entertainment.commands; package creator.loki.feature.entertainment.commands.rating;
import java.awt.Color; import java.awt.Color;
import java.util.List; import java.util.List;

View file

@ -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<Member> 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;
}
}

View file

@ -13,10 +13,10 @@ import creator.loki.core.registry.CommandRegistry;
import creator.loki.embed.EmbedFactory; import creator.loki.embed.EmbedFactory;
import creator.loki.embed.HelpCategories; import creator.loki.embed.HelpCategories;
import net.dv8tion.jda.api.Permission; 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.actionrow.ActionRow;
import net.dv8tion.jda.api.components.selections.StringSelectMenu; 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 @Singleton
public class HelpMenuHandler extends ListenerAdapter { public class HelpMenuHandler extends ListenerAdapter {

View file

@ -12,9 +12,9 @@ import creator.loki.core.registry.CommandRegistry;
import creator.loki.embed.EmbedFactory; import creator.loki.embed.EmbedFactory;
import creator.loki.embed.HelpCategories; import creator.loki.embed.HelpCategories;
import net.dv8tion.jda.api.Permission; 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.actionrow.ActionRow;
import net.dv8tion.jda.api.components.selections.StringSelectMenu; import net.dv8tion.jda.api.components.selections.StringSelectMenu;
import net.dv8tion.jda.api.entities.User;
public class HelpCommand implements Command { public class HelpCommand implements Command {
private final CommandRegistry registry; private final CommandRegistry registry;
@ -75,7 +75,11 @@ public class HelpCommand implements Command {
net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode(emoji)); 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( private EmbedFactory createMainPage(

View file

@ -5,7 +5,6 @@ import java.util.concurrent.ThreadLocalRandom;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import creator.loki.core.EventDispatcher; import creator.loki.core.EventDispatcher;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.economy.service.EconomyService; import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.level.domain.LevelUpResult; import creator.loki.feature.level.domain.LevelUpResult;
import creator.loki.feature.level.service.LevelService; import creator.loki.feature.level.service.LevelService;
@ -20,13 +19,16 @@ public class MessageListener extends ListenerAdapter {
private final EventDispatcher dispatcher; private final EventDispatcher dispatcher;
private final LevelService levelService; private final LevelService levelService;
private final EconomyService economyService; private final EconomyService economyService;
private final LevelUpNotifier levelUpNotifier;
@Inject @Inject
public MessageListener( public MessageListener(
EventDispatcher dispatcher, LevelService levelService, EconomyService economyService) { EventDispatcher dispatcher, LevelService levelService,
EconomyService economyService, LevelUpNotifier levelUpNotifier) {
this.dispatcher = dispatcher; this.dispatcher = dispatcher;
this.levelService = levelService; this.levelService = levelService;
this.economyService = economyService; this.economyService = economyService;
this.levelUpNotifier = levelUpNotifier;
} }
@Override @Override
@ -35,19 +37,10 @@ public class MessageListener extends ListenerAdapter {
if (!event.getAuthor().isBot()) { if (!event.getAuthor().isBot()) {
String userId = event.getAuthor().getId(); String userId = event.getAuthor().getId();
LevelUpResult levelUp = levelService.addMessageXP(userId); LevelUpResult levelUp = levelService.addMessageXP(userId);
if (levelUp != null) { if (levelUp != null && event.getMember() != null) {
event levelUpNotifier.sendLevelUpNotification(
.getChannel() event.getMember(), levelUp.getOldLevel(), levelUp.getNewLevel(),
.sendMessageEmbeds( event.getChannel());
EmbedTemplates.success(
"🎉 Повышение уровня!",
event.getAuthor().getAsMention()
+ " достиг **"
+ levelUp.getNewLevel()
+ " уровня**! Всего XP: **"
+ levelUp.getTotalXP()
+ "**"))
.queue();
} }
int lcAmount = ThreadLocalRandom.current().nextInt(1, 4); int lcAmount = ThreadLocalRandom.current().nextInt(1, 4);
economyService.earnCash(userId, lcAmount); economyService.earnCash(userId, lcAmount);

View file

@ -11,10 +11,12 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** Обработчик кнопок подтверждения сброса уровней */
@Singleton @Singleton
public class ResetLevelsButtonHandler extends ListenerAdapter { public class ResetLevelsButtonHandler extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(ResetLevelsButtonHandler.class); 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; private final ResetLevelsCommand resetCommand;
@Inject @Inject
@ -26,87 +28,47 @@ public class ResetLevelsButtonHandler extends ListenerAdapter {
public void onButtonInteraction(ButtonInteractionEvent event) { public void onButtonInteraction(ButtonInteractionEvent event) {
String buttonId = event.getComponentId(); String buttonId = event.getComponentId();
// Проверяем что это наша кнопка if (buttonId.startsWith(PREFIX_CONFIRM)) {
if (!buttonId.startsWith("reset_levels_")) { String authorId = buttonId.substring(PREFIX_CONFIRM.length());
return; if (authorId.isEmpty()) return;
} if (!event.getUser().getId().equals(authorId)) {
event.reply("❌ Только инициатор команды может подтвердить действие!").setEphemeral(true).queue();
// Извлекаем ID пользователя из кнопки return;
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:")) {
// Подтверждение
event.deferReply().queue(); event.deferReply().queue();
try { try {
String guildId = event.getGuild().getId(); resetCommand.executeReset(event.getGuild().getId(), event.getUser().getId());
String userId = event.getUser().getId();
// Выполняем сброс
resetCommand.executeReset(guildId, userId);
// Удаляем исходное сообщение с кнопками
event.getMessage().delete().queue(); event.getMessage().delete().queue();
event.getHook().sendMessageEmbeds(
// Отправляем подтверждение EmbedFactory.create()
event .withTitle("✅ Сброс уровней выполнен")
.getHook() .withDescription("Уровни и XP всех пользователей успешно сброшены!\n\n"
.sendMessageEmbeds( + "**Сохранено:**\n• Время в голосовых каналах\n"
EmbedFactory.create() + "• Количество сообщений\n• Репутация\n\n"
.withTitle("✅ Сброс уровней выполнен") + "**Сброшено:**\n• Уровень → 0\n• XP → 0\n\n"
.withDescription( + "Теперь используется новая система уровней!")
"Уровни и XP всех пользователей успешно сброшены!\n\n" .withColor(Color.GREEN)
+ "**Сохранено:**\n" .withFooter("Инициатор: " + event.getUser().getName(), event.getUser().getAvatarUrl())
+ "• Время в голосовых каналах\n" .build()).queue();
+ "• Количество сообщений\n" LoggerDecorator.bot(logger, "Сброс уровней подтверждён: " + event.getUser().getName());
+ "• Репутация\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) { } catch (Exception e) {
LoggerDecorator.error(logger, "Ошибка при выполнении сброса", e); LoggerDecorator.error(logger, "Ошибка при выполнении сброса", e);
event event.getHook().sendMessageEmbeds(
.getHook() EmbedFactory.create()
.sendMessageEmbeds( .withTitle("❌ Ошибка")
EmbedFactory.create() .withDescription("Произошла ошибка при сбросе уровней: " + e.getMessage())
.withTitle("❌ Ошибка") .withColor(Color.RED).build()).queue();
.withDescription("Произошла ошибка при сбросе уровней: " + e.getMessage()) }
.withColor(Color.RED) } else if (buttonId.startsWith(PREFIX_CANCEL)) {
.build()) String authorId = buttonId.substring(PREFIX_CANCEL.length());
.queue(); 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.getMessage().delete().queue();
event.reply("❌ Сброс уровней отменён.").setEphemeral(true).queue(); event.reply("❌ Сброс уровней отменён.").setEphemeral(true).queue();
LoggerDecorator.bot(logger, "Сброс уровней отменён: " + event.getUser().getName());
LoggerDecorator.bot(
logger, "Сброс уровней отменён пользователем: " + event.getUser().getName());
} }
} }
} }

View file

@ -9,6 +9,7 @@ import java.util.concurrent.TimeUnit;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import creator.loki.feature.economy.service.EconomyService; import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.level.domain.LevelUpResult;
import creator.loki.feature.level.service.LevelService; import creator.loki.feature.level.service.LevelService;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** Отслеживание времени в голосовых каналах */
@Singleton @Singleton
public class VoiceTracker extends ListenerAdapter { public class VoiceTracker extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(VoiceTracker.class); private static final Logger logger = LoggerFactory.getLogger(VoiceTracker.class);
private final LevelService levelService; private final LevelService levelService;
private final EconomyService economyService; private final EconomyService economyService;
private final LevelUpNotifier levelUpNotifier;
private final Map<String, Long> voiceJoinTimes = new ConcurrentHashMap<>(); private final Map<String, Long> voiceJoinTimes = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private final Object lock = new Object();
@Inject @Inject
public VoiceTracker(LevelService levelService, EconomyService economyService) { public VoiceTracker(LevelService levelService, EconomyService economyService,
LevelUpNotifier levelUpNotifier) {
this.levelService = levelService; this.levelService = levelService;
this.economyService = economyService; this.economyService = economyService;
this.levelUpNotifier = levelUpNotifier;
// Каждую минуту начисляем XP тем, кто в войсе
scheduler.scheduleAtFixedRate( scheduler.scheduleAtFixedRate(
() -> Thread.startVirtualThread(this::updateVoiceTime), 1, 1, TimeUnit.MINUTES); () -> Thread.startVirtualThread(this::updateVoiceTime), 1, 1, TimeUnit.MINUTES);
} }
@ -39,66 +41,48 @@ public class VoiceTracker extends ListenerAdapter {
public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) { public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) {
Member member = event.getMember(); Member member = event.getMember();
String userId = member.getId(); String userId = member.getId();
if (member.getUser().isBot()) return;
// Игнорируем ботов
if (member.getUser().isBot()) {
return;
}
// Пользователь зашёл в войс
if (event.getChannelJoined() != null && event.getChannelLeft() == null) { if (event.getChannelJoined() != null && event.getChannelLeft() == null) {
voiceJoinTimes.put(userId, System.currentTimeMillis()); voiceJoinTimes.put(userId, System.currentTimeMillis());
logger.debug("Пользователь {} зашёл в войс", member.getEffectiveName());
} else if (event.getChannelLeft() != null && event.getChannelJoined() == null) { } else if (event.getChannelLeft() != null && event.getChannelJoined() == null) {
// Пользователь вышел из войса Long joinTime;
Long joinTime = voiceJoinTimes.remove(userId); synchronized (lock) { joinTime = voiceJoinTimes.remove(userId); }
if (joinTime != null) { if (joinTime != null) {
long timeSpent = System.currentTimeMillis() - joinTime; long minutes = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - joinTime);
long minutes = TimeUnit.MILLISECONDS.toMinutes(timeSpent);
if (minutes > 0) { if (minutes > 0) {
levelService.addVoiceXP(userId, minutes); LevelUpResult lr = levelService.addVoiceXP(userId, minutes);
economyService.earnCash(userId, minutes / 2); economyService.earnCash(userId, minutes / 2);
logger.debug( if (lr != null && event.getMember() != null) {
"Пользователь {} провёл в войсе {} минут", member.getEffectiveName(), minutes); levelUpNotifier.sendLevelUpNotification(
event.getMember(), lr.getOldLevel(), lr.getNewLevel(),
event.getGuild().getSystemChannel());
}
} }
} }
} else if (event.getChannelJoined() != null && event.getChannelLeft() != null) { } else if (event.getChannelJoined() != null && event.getChannelLeft() != null) {
// Пользователь переключился между каналами synchronized (lock) { voiceJoinTimes.put(userId, System.currentTimeMillis()); }
// Обновляем время входа
voiceJoinTimes.put(userId, System.currentTimeMillis());
} }
} }
/** Периодическое обновление времени для тех, кто долго в войсе */
private void updateVoiceTime() { private void updateVoiceTime() {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
synchronized (lock) {
for (Map.Entry<String, Long> entry : voiceJoinTimes.entrySet()) { for (Map.Entry<String, Long> entry : voiceJoinTimes.entrySet()) {
String userId = entry.getKey(); String userId = entry.getKey();
long joinTime = entry.getValue(); long minutes = TimeUnit.MILLISECONDS.toMinutes(now - entry.getValue());
long timeSpent = now - joinTime; if (minutes >= 1) {
long minutes = TimeUnit.MILLISECONDS.toMinutes(timeSpent); levelService.addVoiceXP(userId, 1);
economyService.earnCash(userId, 1);
if (minutes >= 1) { entry.setValue(now);
// Начисляем XP за каждую минуту }
levelService.addVoiceXP(userId, 1);
economyService.earnCash(userId, 1);
// Обновляем время входа
voiceJoinTimes.put(userId, now);
} }
} }
} }
public void shutdown() { public void shutdown() {
scheduler.shutdown(); scheduler.shutdown();
try { try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) scheduler.shutdownNow(); }
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); }
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
} }
} }

View file

@ -36,6 +36,7 @@ public class LevelService {
return null; return null;
} }
cooldownService.markMessageSent(userId);
UserStats stats = repository.getUserStats(userId); UserStats stats = repository.getUserStats(userId);
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
int oldLevel = stats.getLevel(); int oldLevel = stats.getLevel();

View file

@ -1,55 +1,45 @@
package creator.loki.feature.level.service; 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.domain.UserStats;
import creator.loki.feature.level.repository.UserStatsRepository; import creator.loki.feature.level.repository.UserStatsRepository;
/**
* Сервис для управления кулдауном сообщений Использует персистентное хранение через
* UserStats.lastMessageTime
*/
public class MessageCooldownService { 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 UserStatsRepository repository;
private final Map<String, Long> lastMessageTimes = new ConcurrentHashMap<>();
public MessageCooldownService(UserStatsRepository repository) { public MessageCooldownService(UserStatsRepository repository) {
this.repository = repository; this.repository = repository;
} }
/**
* Проверить, может ли пользователь получить XP за сообщение
*
* @param userId ID пользователя
* @return true если кулдаун прошел
*/
public boolean canReceiveXP(String userId) { public boolean canReceiveXP(String userId) {
UserStats stats = repository.getUserStats(userId);
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long lastMessage = stats.getLastMessageTime(); Long lastTime = lastMessageTimes.get(userId);
if (lastTime != null && (now - lastTime) < MESSAGE_COOLDOWN_MS) return false;
return lastMessage == 0 || (now - lastMessage) >= MESSAGE_COOLDOWN_MS; 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) { public long getRemainingCooldown(String userId) {
UserStats stats = repository.getUserStats(userId);
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long lastMessage = stats.getLastMessageTime(); Long lastTime = lastMessageTimes.get(userId);
if (lastTime == null) {
if (lastMessage == 0) { long dbTime = repository.getUserStats(userId).getLastMessageTime();
return 0; if (dbTime == 0) return 0;
lastMessageTimes.put(userId, dbTime);
lastTime = dbTime;
} }
long elapsed = now - lastTime;
long elapsed = now - lastMessage; return elapsed >= MESSAGE_COOLDOWN_MS ? 0 : (MESSAGE_COOLDOWN_MS - elapsed) / 1000;
if (elapsed >= MESSAGE_COOLDOWN_MS) {
return 0;
}
return (MESSAGE_COOLDOWN_MS - elapsed) / 1000;
} }
} }

View file

@ -10,16 +10,10 @@ import creator.loki.util.LoggerDecorator;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** Сборщик метрик для мониторинга бота */
public class MetricsCollector { public class MetricsCollector {
private static final Logger logger = LoggerFactory.getLogger(MetricsCollector.class); private static final Logger logger = LoggerFactory.getLogger(MetricsCollector.class);
// Счётчики команд private final ConcurrentHashMap<String, CommandEntry> commands = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LongAdder> commandCounts = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LongAdder> commandErrors = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicLong> commandTotalTime = new ConcurrentHashMap<>();
// Общие метрики
private final LongAdder totalCommands = new LongAdder(); private final LongAdder totalCommands = new LongAdder();
private final LongAdder totalErrors = new LongAdder(); private final LongAdder totalErrors = new LongAdder();
private final AtomicLong startTime = new AtomicLong(System.currentTimeMillis()); private final AtomicLong startTime = new AtomicLong(System.currentTimeMillis());
@ -28,79 +22,60 @@ public class MetricsCollector {
LoggerDecorator.bot(logger, "MetricsCollector инициализирован"); LoggerDecorator.bot(logger, "MetricsCollector инициализирован");
} }
/** Записывает выполнение команды */
public void recordCommand(String commandName, long executionTimeMs) { public void recordCommand(String commandName, long executionTimeMs) {
commandCounts.computeIfAbsent(commandName, k -> new LongAdder()).increment(); var entry = commands.computeIfAbsent(commandName, k -> new CommandEntry());
commandTotalTime.computeIfAbsent(commandName, k -> new AtomicLong()).addAndGet(executionTimeMs); entry.count.increment();
entry.totalTime.addAndGet(executionTimeMs);
totalCommands.increment(); totalCommands.increment();
} }
/** Записывает ошибку команды */
public void recordError(String commandName) { 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(); totalErrors.increment();
} }
/** Возвращает количество выполнений команды */
public long getCommandCount(String commandName) { public long getCommandCount(String commandName) {
LongAdder counter = commandCounts.get(commandName); var entry = commands.get(commandName);
return counter != null ? counter.sum() : 0; return entry != null ? entry.count.sum() : 0;
} }
/** Возвращает количество ошибок команды */
public long getCommandErrors(String commandName) { public long getCommandErrors(String commandName) {
LongAdder counter = commandErrors.get(commandName); var entry = commands.get(commandName);
return counter != null ? counter.sum() : 0; return entry != null ? entry.errors.sum() : 0;
} }
/** Возвращает среднее время выполнения команды */
public double getAverageExecutionTime(String commandName) { public double getAverageExecutionTime(String commandName) {
long count = getCommandCount(commandName); var entry = commands.get(commandName);
if (count == 0) { var count = entry != null ? entry.count.sum() : 0;
return 0.0; return count == 0 ? 0.0 : (double) entry.totalTime.get() / count;
}
AtomicLong totalTime = commandTotalTime.get(commandName);
if (totalTime == null) {
return 0.0;
}
return (double) totalTime.get() / count;
} }
/** Возвращает общее количество выполненных команд */
public long getTotalCommands() { public long getTotalCommands() {
return totalCommands.sum(); return totalCommands.sum();
} }
/** Возвращает общее количество ошибок */
public long getTotalErrors() { public long getTotalErrors() {
return totalErrors.sum(); return totalErrors.sum();
} }
/** Возвращает процент ошибок */
public double getErrorRate() { public double getErrorRate() {
long total = getTotalCommands(); return getTotalCommands() == 0 ? 0.0 : (double) getTotalErrors() / getTotalCommands() * 100;
if (total == 0) {
return 0.0;
}
return (double) getTotalErrors() / total * 100;
} }
/** Возвращает время работы бота в секундах */
public long getUptimeSeconds() { public long getUptimeSeconds() {
return (System.currentTimeMillis() - startTime.get()) / 1000; return (System.currentTimeMillis() - startTime.get()) / 1000;
} }
/** JSON для {@code GET /health} (пробы оркестраторов). */
public String buildHealthJson() { public String buildHealthJson() {
String version =
Optional.ofNullable(MetricsCollector.class.getPackage().getImplementationVersion())
.filter(v -> !v.isBlank())
.orElse("dev");
return String.format( return String.format(
Locale.ROOT, Locale.ROOT,
"{\"status\":\"ok\",\"version\":%s,\"uptimeSeconds\":%d,\"commandsRun\":%d," "{\"status\":\"ok\",\"version\":%s,\"uptimeSeconds\":%d,\"commandsRun\":%d,"
+ "\"errors\":%d,\"errorRatePercent\":%.4f}", + "\"errors\":%d,\"errorRatePercent\":%.4f}",
jsonQuoted(version), jsonQuoted(
Optional.ofNullable(MetricsCollector.class.getPackage().getImplementationVersion())
.filter(v -> !v.isBlank())
.orElse("dev")),
getUptimeSeconds(), getUptimeSeconds(),
getTotalCommands(), getTotalCommands(),
getTotalErrors(), getTotalErrors(),
@ -108,75 +83,61 @@ public class MetricsCollector {
} }
private static String jsonQuoted(String s) { private static String jsonQuoted(String s) {
StringBuilder sb = new StringBuilder(s.length() + 2); return '"'
sb.append('"'); + s.replace("\\", "\\\\")
for (int i = 0; i < s.length(); i++) { .replace("\"", "\\\"")
char c = s.charAt(i); .replace("\n", "\\n")
switch (c) { .replace("\r", "\\r")
case '\\' -> sb.append("\\\\"); .replace("\t", "\\t")
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();
} }
/** Возвращает топ команд по использованию */
public String getTopCommands(int limit) { public String getTopCommands(int limit) {
StringBuilder sb = new StringBuilder(); var sb = new StringBuilder();
sb.append("📊 Топ команд:\n"); sb.append("📊 Топ команд:\n");
commands.entrySet().stream()
commandCounts.entrySet().stream() .sorted((e1, e2) -> Long.compare(e2.getValue().count.sum(), e1.getValue().count.sum()))
.sorted((e1, e2) -> Long.compare(e2.getValue().sum(), e1.getValue().sum()))
.limit(limit) .limit(limit)
.forEach( .forEach(
entry -> { entry -> {
String cmd = entry.getKey(); var ce = entry.getValue();
long count = entry.getValue().sum(); var count = ce.count.sum();
long errors = getCommandErrors(cmd); var avgTime = count == 0 ? 0.0 : (double) ce.totalTime.get() / count;
double avgTime = getAverageExecutionTime(cmd);
sb.append( sb.append(
String.format( String.format(
" %s: %d выполнений, %.1f мс среднее, %d ошибок%n", " %s: %d выполнений, %.1f мс среднее, %d ошибок%n",
cmd, count, avgTime, errors)); entry.getKey(), count, avgTime, ce.errors.sum()));
}); });
return sb.toString(); return sb.toString();
} }
/** Возвращает полную статистику */
public String getFullStats() { public String getFullStats() {
long uptime = getUptimeSeconds(); var sb = new StringBuilder();
long hours = uptime / 3600;
long minutes = (uptime % 3600) / 60;
long seconds = uptime % 60;
StringBuilder sb = new StringBuilder();
sb.append("📈 Статистика бота\n"); sb.append("📈 Статистика бота\n");
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%n", getTotalCommands()));
sb.append(String.format("❌ Всего ошибок: %d (%.2f%%)%n", getTotalErrors(), getErrorRate())); 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("\n");
sb.append(getTopCommands(10)); sb.append(getTopCommands(10));
return sb.toString(); return sb.toString();
} }
/** Сбрасывает все метрики */
public void reset() { public void reset() {
commandCounts.clear(); commands.clear();
commandErrors.clear();
commandTotalTime.clear();
totalCommands.reset(); totalCommands.reset();
totalErrors.reset(); totalErrors.reset();
startTime.set(System.currentTimeMillis()); startTime.set(System.currentTimeMillis());
LoggerDecorator.bot(logger, "Метрики сброшены"); LoggerDecorator.bot(logger, "Метрики сброшены");
} }
private static class CommandEntry {
final LongAdder count = new LongAdder();
final LongAdder errors = new LongAdder();
final AtomicLong totalTime = new AtomicLong();
}
} }

View file

@ -6,12 +6,13 @@ import creator.loki.core.registry.CommandRegistry;
import creator.loki.feature.config.repository.ConfigRepository; import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.config.repository.GuildConfigRepository; import creator.loki.feature.config.repository.GuildConfigRepository;
import creator.loki.feature.logging.LogSetupPrefixCommand; 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.MuteCommand;
import creator.loki.feature.moderation.commands.punish.PunishCommand; import creator.loki.feature.moderation.commands.punish.PunishCommand;
import creator.loki.feature.moderation.commands.punish.UnmuteCommand; 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.ClearCommand;
import creator.loki.feature.moderation.commands.utils.SlowmodeCommand; 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.UnwarnCommand;
import creator.loki.feature.moderation.commands.warn.WarnCommand; import creator.loki.feature.moderation.commands.warn.WarnCommand;
import creator.loki.feature.moderation.commands.warn.WarningsHistoryCommand; 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 WarningsHistoryCommand(warningRepository));
registry.register(new ClearCommand()); registry.register(new ClearCommand());
registry.register(new SlowmodeCommand()); registry.register(new SlowmodeCommand());
registry.register(SwapAndCaseCommand.createSwap()); registry.register(new SwapCommand());
registry.register(SwapAndCaseCommand.createCaseLookup(moderationCaseRepository)); registry.register(new CaseLookupCommand(moderationCaseRepository));
registry.register(new CreateRoleCommand(roleManager)); registry.register(new CreateRoleCommand(roleManager));
registry.register(new DeleteRoleCommand(roleManager)); registry.register(new DeleteRoleCommand(roleManager));

View file

@ -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 <id>";
}
@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("Время", "<t:" + (c.createdAt() / 1000) + ":F>", false)
.build())
.queue(),
() -> ModerationHelper.error(ctx, "Кейса `" + caseId + "` на этом сервере нет"));
}
}

View file

@ -6,8 +6,8 @@ import java.time.OffsetDateTime;
import creator.loki.command.Command; import creator.loki.command.Command;
import creator.loki.command.CommandContext; import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory; import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.config.repository.ConfigRepository; 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.MuteRoleService;
import creator.loki.feature.moderation.MuteScheduler; import creator.loki.feature.moderation.MuteScheduler;
import creator.loki.feature.moderation.repository.ActiveMuteRepository; import creator.loki.feature.moderation.repository.ActiveMuteRepository;
@ -190,10 +190,10 @@ public class MuteCommand implements Command {
} }
private static String sub(String s, int max) { 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) { private static void err(CommandContext ctx, String msg) {
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue(); ModerationHelper.error(ctx, msg);
} }
} }

View file

@ -5,6 +5,7 @@ import java.util.concurrent.TimeUnit;
import creator.loki.command.Command; import creator.loki.command.Command;
import creator.loki.command.CommandContext; import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedTemplates; import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.moderation.util.ModerationHelper;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
@ -123,6 +124,6 @@ public final class PunishCommand {
} }
private static void err(CommandContext ctx, String msg) { private static void err(CommandContext ctx, String msg) {
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue(); ModerationHelper.error(ctx, msg);
} }
} }

View file

@ -13,6 +13,7 @@ import creator.loki.feature.moderation.MuteRoleService;
import creator.loki.feature.moderation.MuteScheduler; import creator.loki.feature.moderation.MuteScheduler;
import creator.loki.feature.moderation.repository.ActiveMuteRepository; import creator.loki.feature.moderation.repository.ActiveMuteRepository;
import creator.loki.feature.moderation.repository.ModerationCaseRepository; 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.EmbedBuilder;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
@ -137,10 +138,10 @@ public class UnmuteCommand implements Command {
} }
private static String trunc(String s, int max) { 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) { private static void err(CommandContext ctx, String msg) {
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue(); ModerationHelper.error(ctx, msg);
} }
} }

View file

@ -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());
}
});
}
}

View file

@ -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 <id>";
}
@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("Время", "<t:" + (c.createdAt() / 1000) + ":F>", 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();
}
}

View file

@ -13,6 +13,7 @@ import creator.loki.feature.moderation.MuteScheduler;
import creator.loki.feature.moderation.repository.ActiveMuteRepository; import creator.loki.feature.moderation.repository.ActiveMuteRepository;
import creator.loki.feature.moderation.repository.ModerationCaseRepository; import creator.loki.feature.moderation.repository.ModerationCaseRepository;
import creator.loki.feature.moderation.repository.WarningRepository; 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.EmbedBuilder;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
@ -66,38 +67,46 @@ public class WarnCommand implements Command {
@Override @Override
public void execute(CommandContext ctx) { public void execute(CommandContext ctx) {
var t = validateInput(ctx);
if (t != null) saveWarning(ctx, t);
}
private Member validateInput(CommandContext ctx) {
var g = ctx.getGuild(); var g = ctx.getGuild();
if (g == null) { if (g == null) {
err(ctx, "Только на сервере"); ModerationHelper.error(ctx, "Только на сервере");
return; return null;
} }
var mod = ctx.getAuthor(); var mod = ctx.getAuthor();
if (mod == null || !mod.hasPermission(Permission.KICK_MEMBERS)) { if (mod == null || !mod.hasPermission(Permission.KICK_MEMBERS)) {
err(ctx, "Нужно **Kick Members**"); ModerationHelper.error(ctx, "Нужно **Kick Members**");
return; return null;
} }
var mentioned = ctx.getMessage().getMentions().getMembers(); var mentioned = ctx.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) { if (mentioned.isEmpty()) {
err(ctx, "Укажи пользователя: `" + getUsage() + "`"); ModerationHelper.error(ctx, "Укажи пользователя: `" + getUsage() + "`");
return; return null;
} }
var t = mentioned.get(0); var t = mentioned.get(0);
if (t.getId().equals(mod.getId())) { if (t.getId().equals(mod.getId())) {
err(ctx, "Нельзя выдать варн самому себе"); ModerationHelper.error(ctx, "Нельзя выдать варн самому себе");
return; return null;
} }
if (!mod.canInteract(t)) { if (!mod.canInteract(t)) {
err(ctx, "Участник выше ваших ролей"); ModerationHelper.error(ctx, "Участник выше ваших ролей");
return; return null;
} }
var reason = return t;
ctx.getArgs().size() > 1 }
? ctx.getArgs().stream().skip(1).collect(Collectors.joining(" ")).trim()
: "Без причины"; 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 = "Без причины"; if (reason.isEmpty()) reason = "Без причины";
long id = warnings.insert(g.getId(), t.getId(), mod.getId(), reason); long id = warnings.insert(g.getId(), t.getId(), mod.getId(), reason);
if (id < 0) { if (id < 0) {
err(ctx, "Не удалось сохранить предупреждение"); ModerationHelper.error(ctx, "Не удалось сохранить предупреждение");
return; return;
} }
int total = warnings.countByUser(g.getId(), t.getId()); int total = warnings.countByUser(g.getId(), t.getId());
@ -110,7 +119,7 @@ public class WarnCommand implements Command {
+ "\n**ID записи:** `" + "\n**ID записи:** `"
+ id + id
+ "`\n**Причина:** " + "`\n**Причина:** "
+ trunc(reason, 500) + ModerationHelper.trunc(reason, 500)
+ "\n**Всего варнов:** " + "\n**Всего варнов:** "
+ total)) + total))
.queue(); .queue();
@ -133,7 +142,7 @@ public class WarnCommand implements Command {
.addField("Модератор", mod.getAsMention(), false) .addField("Модератор", mod.getAsMention(), false)
.addField("ID записи", "`" + warnId + "`", true) .addField("ID записи", "`" + warnId + "`", true)
.addField("Всего варнов", String.valueOf(total), true) .addField("Всего варнов", String.valueOf(total), true)
.addField("Причина", trunc(reason, 1000), false) .addField("Причина", ModerationHelper.trunc(reason, 1000), false)
.setTimestamp(OffsetDateTime.now()) .setTimestamp(OffsetDateTime.now())
.build()) .build())
.queue(s -> {}, e -> {}); .queue(s -> {}, e -> {});
@ -148,7 +157,7 @@ public class WarnCommand implements Command {
} }
var self = g.getMember(g.getJDA().getSelfUser()); var self = g.getMember(g.getJDA().getSelfUser());
if (!self.canInteract(t) || !self.canInteract(muteRole)) return; if (!self.canInteract(t) || !self.canInteract(muteRole)) return;
var reason = "Авто-мут: 3+ варна"; String reason = "Авто-мут: 3+ варна";
long untilMs = System.currentTimeMillis() + AUTO_MUTE_HOURS * 3600 * 1000; long untilMs = System.currentTimeMillis() + AUTO_MUTE_HOURS * 3600 * 1000;
long durSec = AUTO_MUTE_HOURS * 3600; long durSec = AUTO_MUTE_HOURS * 3600;
long caseId = cases.insert(g.getId(), "MUTE", t.getId(), mod.getId(), reason, durSec, untilMs); 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())); 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();
}
} }

View file

@ -12,7 +12,6 @@ import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.audit.ActionType; import net.dv8tion.jda.api.audit.ActionType;
import net.dv8tion.jda.api.audit.AuditLogEntry; import net.dv8tion.jda.api.audit.AuditLogEntry;
import net.dv8tion.jda.api.entities.Guild; 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.GuildBanEvent;
import net.dv8tion.jda.api.events.guild.GuildUnbanEvent; import net.dv8tion.jda.api.events.guild.GuildUnbanEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
@ -35,165 +34,118 @@ public class ModerationLogger extends ListenerAdapter {
} }
private void handle( private void handle(
Guild guild, ActionType type, int limit, Function<List<AuditLogEntry>, EmbedBuilder> fn) { Guild g, ActionType t, int l, Function<List<AuditLogEntry>, EmbedBuilder> fn) {
var log = getLogChannel(guild); var log = g.getTextChannelById(configRepo.get("moderation.log_channel_id"));
if (log == null) return; if (log == null) return;
guild var al = g.retrieveAuditLogs().type(t).limit(l);
.retrieveAuditLogs() al.queue(
.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,
logs -> { logs -> {
var entry = logs.isEmpty() ? null : logs.get(0); var embed = fn.apply(logs);
return LogEmbeds.messageDelete(entry, event.getChannel().getAsMention(), content); if (embed == null) return;
log.sendMessageEmbeds(embed.setTimestamp(OffsetDateTime.now()).build()).queue();
}); });
} }
public void onMessageBulkDelete(MessageBulkDeleteEvent event) { public void onMessageDelete(MessageDeleteEvent e) {
handle( var content = messageCache.getContent(e.getMessageId());
event.getGuild(), handle(e.getGuild(), ActionType.MESSAGE_DELETE, 1, l -> md(e, content, l));
ActionType.MESSAGE_BULK_DELETE,
1,
logs -> {
var entry = logs.isEmpty() ? null : logs.get(0);
return LogEmbeds.messageBulkDelete(
entry, event.getChannel().getAsMention(), event.getMessageIds().size());
});
} }
@Override private EmbedBuilder md(MessageDeleteEvent e, String c, List<AuditLogEntry> l) {
public void onGuildBan(GuildBanEvent event) { var entry = l.isEmpty() ? null : l.get(0);
handle( return LogEmbeds.messageDelete(entry, e.getChannel().getAsMention(), c);
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());
});
} }
@Override public void onMessageBulkDelete(MessageBulkDeleteEvent e) {
public void onGuildUnban(GuildUnbanEvent event) { handle(e.getGuild(), ActionType.MESSAGE_BULK_DELETE, 1, l -> mbd(e, l));
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());
});
} }
@Override private EmbedBuilder mbd(MessageBulkDeleteEvent e, List<AuditLogEntry> l) {
public void onGuildMemberRemove(GuildMemberRemoveEvent event) { var entry = l.isEmpty() ? null : l.get(0);
handle( var size = e.getMessageIds().size();
event.getGuild(), return LogEmbeds.messageBulkDelete(entry, e.getChannel().getAsMention(), size);
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());
});
} }
@Override public void onGuildBan(GuildBanEvent ev) {
public void onGuildMemberUpdateTimeOut(GuildMemberUpdateTimeOutEvent event) { handle(ev.getGuild(), ActionType.BAN, 1, l -> b(ev, l));
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());
});
} }
@Override private EmbedBuilder b(GuildBanEvent ev, List<AuditLogEntry> l) {
public void onGuildVoiceGuildMute(GuildVoiceGuildMuteEvent event) { if (l.isEmpty()) return null;
var muted = event.isGuildMuted(); var user = ev.getUser();
handle( var e = l.get(0);
event.getGuild(), var mod = e.getUser() != null ? e.getUser().getAsMention() : "Неизвестно";
ActionType.MEMBER_UPDATE, var reason = e.getReason() != null ? e.getReason() : "Не указана";
5, return LogEmbeds.ban(user.getAsMention(), mod, reason, user.getAvatarUrl());
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);
});
} }
@Override public void onGuildUnban(GuildUnbanEvent ev) {
public void onGuildVoiceGuildDeafen(GuildVoiceGuildDeafenEvent event) { handle(ev.getGuild(), ActionType.UNBAN, 1, l -> ub(ev, l));
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);
});
} }
private TextChannel getLogChannel(Guild guild) { private EmbedBuilder ub(GuildUnbanEvent ev, List<AuditLogEntry> l) {
var logChannelId = configRepo.get("moderation.log_channel_id"); if (l.isEmpty()) return null;
return logChannelId != null ? guild.getTextChannelById(logChannelId) : 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<AuditLogEntry> 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<AuditLogEntry> 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<AuditLogEntry> 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<AuditLogEntry> 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<AuditLogEntry> 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);
} }
} }

View file

@ -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();
}
}

View file

@ -161,7 +161,10 @@ public class FilterCommands {
public void execute(CommandContext ctx) { public void execute(CommandContext ctx) {
var m = mgr(pm, ctx); var m = mgr(pm, ctx);
if (!checkChannel(m, ctx)) return; 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); if (isNight) m.getScheduler().setNightcore(enabled);
else m.getScheduler().setVaporwave(enabled); else m.getScheduler().setVaporwave(enabled);
ctx.getChannel() ctx.getChannel()

View file

@ -1,207 +1,149 @@
package creator.loki.feature.music.commands; 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.awt.Color;
import java.util.List; import java.util.function.BiConsumer;
import creator.loki.command.Command; import creator.loki.command.Command;
import creator.loki.command.CommandContext; import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory; import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.music.core.PlayerManager; import creator.loki.feature.music.core.PlayerManager;
import dev.arbjerg.lavalink.client.player.Track; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class QueueCommands { public class QueueCommands {
private static final Color C = new Color(88, 101, 242);
private static final Logger LOG = LoggerFactory.getLogger(QueueCommands.class);
private QueueCommands() {} private QueueCommands() {}
public static Command queue(PlayerManager pm) { 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) { public static Command nowplaying(PlayerManager pm) {
return new NowPlaying(pm); BiConsumer<PlayerManager, CommandContext> exec = QueueCommands::executeNowPlaying;
return new Mc(pm, "nowplaying", "Показать текущий трек", "nowplaying", "Музыка", exec);
} }
public static Command volume(PlayerManager pm) { public static Command volume(PlayerManager pm) {
return new Volume(pm); BiConsumer<PlayerManager, CommandContext> exec = QueueCommands::executeVolume;
return new Mc(pm, "volume", "Регулировка громкости (0-100)", "volume [0-100]", "Музыка", exec);
} }
private static final class Queue implements Command { private static final class Mc implements Command {
private final PlayerManager pm; private final PlayerManager p;
private final String n, d, u, c;
private final BiConsumer<PlayerManager, CommandContext> e;
Queue(PlayerManager pm) { Mc(
this.pm = pm; PlayerManager p,
String n,
String d,
String u,
String c,
BiConsumer<PlayerManager, CommandContext> e) {
this.p = p;
this.n = n;
this.d = d;
this.u = u;
this.c = c;
this.e = e;
} }
@Override
public String getName() { public String getName() {
return "queue"; return n;
} }
@Override
public String getDescription() { 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<Track> q = m.getScheduler().getQueue();
if (cur == null && q.isEmpty()) {
ctx.getChannel().sendMessage("Очередь пуста.").queue();
return;
}
var embed =
EmbedFactory.create().withTitle("Очередь треков").withColor(new Color(88, 101, 242));
if (cur != null) {
embed.addField(
"Сейчас играет",
"**" + cur.getInfo().getTitle() + "**\n" + cur.getInfo().getAuthor(),
false);
}
if (!q.isEmpty()) {
var sb = new StringBuilder();
int show = Math.min(q.size(), 10);
for (int i = 0; i < show; i++) {
sb.append("`")
.append(i + 1)
.append(".` **")
.append(q.get(i).getInfo().getTitle())
.append("**\n");
}
if (q.size() > show) {
sb.append("... и ещё **").append(q.size() - show).append("** треков");
}
embed.addField("В очереди (" + q.size() + ")", sb.toString(), false);
}
ctx.getChannel().sendMessageEmbeds(embed.build()).queue();
}
}
private static final class NowPlaying implements Command {
private final PlayerManager pm;
NowPlaying(PlayerManager pm) {
this.pm = pm;
}
@Override
public String getName() {
return "nowplaying";
}
@Override
public String getDescription() {
return "Показать текущий трек";
}
@Override
public String getUsage() { public String getUsage() {
return "nowplaying"; return u != null ? u : n;
} }
@Override
public String getCategory() { public String getCategory() {
return "Музыка"; return c != null ? c : "Общие";
} }
@Override
public void execute(CommandContext ctx) { public void execute(CommandContext ctx) {
var lm = pm.getLavalinkManager(); e.accept(p, ctx);
var track = lm.getMusicManager(ctx.getGuild().getIdLong()).getScheduler().getCurrentTrack(); }
if (track == null) { }
ctx.getChannel().sendMessage("Сейчас ничего не играет.").queue();
return; private static void executeQueue(PlayerManager pm, CommandContext ctx) {
} var m = pm.getMusicManager(ctx.getGuild().getIdLong());
var link = lm.getClient().getLinkIfCached(ctx.getGuild().getIdLong()); var cur = m.getScheduler().getCurrentTrack();
long pos = var q = m.getScheduler().getQueue();
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getPosition() : 0; if (cur == null && q.isEmpty()) {
long dur = track.getInfo().getLength(); ctx.getChannel().sendMessage("Очередь пуста.").queue();
long ps = pos / 1000; return;
long ds = dur / 1000; }
if (ds <= 0) ds = 1; var embed = EmbedFactory.create().withTitle("Очередь треков").withColor(C);
int filled = (int) ((double) pos / dur * 20); if (cur != null) {
StringBuilder bar = new StringBuilder("\u2503"); var i = cur.getInfo();
for (int i = 0; i < 20; i++) bar.append(i < filled ? "\u2588" : "\u2591"); embed.addField("Сейчас играет", "**" + i.getTitle() + "**\n" + i.getAuthor(), false);
String pbar = }
bar if (!q.isEmpty()) {
+ "\u2503 " var sb = new StringBuilder();
+ String.format("%d:%02d", ps / 60, ps % 60) var show = Math.min(q.size(), 10);
+ " / " for (var i = 0; i < show; i++)
+ String.format("%d:%02d", ds / 60, ds % 60); sb.append(String.format("`%d.` **%s**\n", i + 1, q.get(i).getInfo().getTitle()));
if (q.size() > show) sb.append("... и ещё **").append(q.size() - show).append("** треков");
embed.addField("В очереди (" + q.size() + ")", sb.toString(), false);
}
ctx.getChannel().sendMessageEmbeds(embed.build()).queue();
}
private static void executeNowPlaying(PlayerManager pm, CommandContext ctx) {
var lm = pm.getLavalinkManager();
var track = lm.getMusicManager(ctx.getGuild().getIdLong()).getScheduler().getCurrentTrack();
if (track == null) {
ctx.getChannel().sendMessage("Сейчас ничего не играет.").queue();
return;
}
var l = lm.getClient().getLinkIfCached(ctx.getGuild().getIdLong());
var pos = l != null && l.getCachedPlayer() != null ? l.getCachedPlayer().getPosition() : 0L;
var dur = track.getInfo().getLength();
var ds = dur / 1000 <= 0 ? 1 : dur / 1000;
var filled = (int) ((double) pos / dur * 20);
var bar = new StringBuilder("\u2503");
for (var i = 0; i < 20; i++) bar.append(i < filled ? "\u2588" : "\u2591");
var t0 = String.format("%d:%02d", pos / 1000 / 60, pos / 1000 % 60);
var t1 = String.format("%d:%02d", ds / 60, ds % 60);
var pbar = bar + "\u2503 " + t0 + " / " + t1;
var msg =
EmbedFactory.create()
.withTitle("Сейчас играет")
.withColor(C)
.addField("Название", "**" + track.getInfo().getTitle() + "**", false)
.addField("Автор", track.getInfo().getAuthor(), true)
.addField("Длительность", pbar, true)
.build();
ctx.getChannel().sendMessageEmbeds(msg).queue();
}
private static void executeVolume(PlayerManager pm, CommandContext ctx) {
var l = pm.getLavalinkManager().getClient().getLinkIfCached(ctx.getGuild().getIdLong());
if (ctx.getArgs().isEmpty()) {
var v = l != null && l.getCachedPlayer() != null ? l.getCachedPlayer().getVolume() : 100;
ctx.getChannel().sendMessageEmbeds(info("Громкость", "Текущая: **" + v + "%**")).queue();
return;
}
try {
var vol = Math.max(0, Math.min(100, Integer.parseInt(ctx.getArgs().get(0))));
if (l != null && l.getCachedPlayer() != null)
l.createOrUpdatePlayer()
.setVolume(vol)
.subscribe(null, err -> LOG.warn("Lavalink: {}", err.getMessage()));
ctx.getChannel() ctx.getChannel()
.sendMessageEmbeds( .sendMessageEmbeds(info("Громкость", "Установлена на **" + vol + "%**"))
EmbedFactory.create()
.withTitle("Сейчас играет")
.withColor(new Color(88, 101, 242))
.addField("Название", "**" + track.getInfo().getTitle() + "**", false)
.addField("Автор", track.getInfo().getAuthor(), true)
.addField("Длительность", pbar, true)
.build())
.queue(); .queue();
} } catch (NumberFormatException e) {
} ctx.getChannel().sendMessageEmbeds(error("Аргументы", "Нужно число от 0 до 100.")).queue();
private static final class Volume implements Command {
private final PlayerManager pm;
Volume(PlayerManager pm) {
this.pm = pm;
}
@Override
public String getName() {
return "volume";
}
@Override
public String getDescription() {
return "Регулировка громкости (0-100)";
}
@Override
public String getUsage() {
return "volume [0-100]";
}
@Override
public String getCategory() {
return "Музыка";
}
@Override
public void execute(CommandContext ctx) {
var lm = pm.getLavalinkManager();
var link = lm.getClient().getLinkIfCached(ctx.getGuild().getIdLong());
if (ctx.getArgs().isEmpty()) {
int v =
link != null && link.getCachedPlayer() != null
? link.getCachedPlayer().getVolume()
: 100;
ctx.getChannel()
.sendMessageEmbeds(EmbedTemplates.info("Громкость", "Текущая: **" + v + "%**"))
.queue();
return;
}
try {
var vol = Math.max(0, Math.min(100, Integer.parseInt(ctx.getArgs().get(0))));
if (link != null && link.getCachedPlayer() != null) {
link.createOrUpdatePlayer()
.setVolume(vol)
.subscribe(
null,
err ->
LoggerFactory.getLogger(Volume.class).warn("Lavalink: {}", err.getMessage()));
}
ctx.getChannel()
.sendMessageEmbeds(EmbedTemplates.info("Громкость", "Установлена на **" + vol + "%**"))
.queue();
} catch (NumberFormatException e) {
ctx.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Аргументы", "Нужно число от 0 до 100."))
.queue();
}
} }
} }
} }

View file

@ -92,7 +92,7 @@ public class LavalinkManager {
event -> { event -> {
GuildMusicManager manager = musicManagers.get(event.getGuildId()); GuildMusicManager manager = musicManagers.get(event.getGuildId());
if (manager != null) { if (manager != null) {
manager.getScheduler().onTrackStart(event.getTrack()); manager.cancelAutoLeaveTimer();
} }
}); });

View file

@ -3,20 +3,21 @@ package creator.loki.feature.music.listener;
import java.util.List; import java.util.List;
import creator.loki.feature.music.core.LavalinkManager; import creator.loki.feature.music.core.LavalinkManager;
import creator.loki.feature.music.core.PlayerManager;
import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class MusicBotControlHandler { public class MusicBotControlHandler {
private final LavalinkManager lavalinkManager; private static final Logger log = LoggerFactory.getLogger(MusicBotControlHandler.class);
private final PlayerManager playerManager; private static final int ERR = 0xFF4444;
public MusicBotControlHandler(LavalinkManager lavalinkManager, PlayerManager playerManager) { private final LavalinkManager lavalinkManager;
public MusicBotControlHandler(LavalinkManager lavalinkManager) {
this.lavalinkManager = lavalinkManager; this.lavalinkManager = lavalinkManager;
this.playerManager = playerManager;
} }
public void handleSkip(MessageChannel ch, Guild guild, Member member) { public void handleSkip(MessageChannel ch, Guild guild, Member member) {
@ -26,7 +27,7 @@ public class MusicBotControlHandler {
send(ch, err("Нечего пропускать.")); send(ch, err("Нечего пропускать."));
return; return;
} }
String t = s.getCurrentTrack().getInfo().getTitle(); var t = s.getCurrentTrack().getInfo().getTitle();
s.skip(); s.skip();
send(ch, ok("⏭ Пропущено", "**" + t + "**")); send(ch, ok("⏭ Пропущено", "**" + t + "**"));
} }
@ -35,7 +36,7 @@ public class MusicBotControlHandler {
if (reqVoice(ch, member) == null) return; if (reqVoice(ch, member) == null) return;
lavalinkManager.getMusicManager(guild.getIdLong()).stop(); lavalinkManager.getMusicManager(guild.getIdLong()).stop();
guild.getJDA().getDirectAudioController().disconnect(guild); guild.getJDA().getDirectAudioController().disconnect(guild);
send(ch, ok(0xFF4444, "⏹ Остановлено", "Музыка остановлена, очередь очищена.")); send(ch, ok(ERR, "⏹ Остановлено", "Музыка остановлена, очередь очищена."));
} }
public void handleQueue(MessageChannel ch, Guild guild) { public void handleQueue(MessageChannel ch, Guild guild) {
@ -47,18 +48,15 @@ public class MusicBotControlHandler {
return; return;
} }
var sb = new StringBuilder(); var sb = new StringBuilder();
if (cur != null) { if (cur != null) sb.append("**Сейчас:** ").append(cur.getInfo().getTitle()).append("\n");
sb.append("**Сейчас:** ").append(cur.getInfo().getTitle()).append("\n");
}
if (!q.isEmpty()) { if (!q.isEmpty()) {
int show = Math.min(q.size(), 10); var show = Math.min(q.size(), 10);
for (int i = 0; i < show; i++) { for (var i = 0; i < show; i++)
sb.append("`") sb.append("`")
.append(i + 1) .append(i + 1)
.append(".` ") .append(".` ")
.append(q.get(i).getInfo().getTitle()) .append(q.get(i).getInfo().getTitle())
.append("\n"); .append("\n");
}
if (q.size() > show) sb.append("... и ещё **").append(q.size() - show).append("** треков"); if (q.size() > show) sb.append("... и ещё **").append(q.size() - show).append("** треков");
} }
send(ch, ok("📜 Очередь", sb.toString())); send(ch, ok("📜 Очередь", sb.toString()));
@ -79,22 +77,23 @@ public class MusicBotControlHandler {
if (reqVoice(ch, member) == null) return; if (reqVoice(ch, member) == null) return;
var link = lavalinkManager.getClient().getLinkIfCached(guild.getIdLong()); var link = lavalinkManager.getClient().getLinkIfCached(guild.getIdLong());
if (args.isEmpty()) { if (args.isEmpty()) {
int v = send(
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getVolume() : 100; ch,
send(ch, ok("🔊 Громкость", "Текущая: **" + v + "%**")); ok(
"🔊 Громкость",
"Текущая: **"
+ (link != null && link.getCachedPlayer() != null
? link.getCachedPlayer().getVolume()
: 100)
+ "%**"));
return; return;
} }
try { try {
int vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0)))); var vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0))));
if (link != null && link.getCachedPlayer() != null) { if (link != null && link.getCachedPlayer() != null)
link.createOrUpdatePlayer() link.createOrUpdatePlayer()
.setVolume(vol) .setVolume(vol)
.subscribe( .subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage()));
null,
err ->
LoggerFactory.getLogger(MusicBotControlHandler.class)
.warn("Lavalink: {}", err.getMessage()));
}
send(ch, ok("🔊 Громкость", "Установлена **" + vol + "%**")); send(ch, ok("🔊 Громкость", "Установлена **" + vol + "%**"));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
send(ch, err("Укажи число от 0 до 100.")); send(ch, err("Укажи число от 0 до 100."));
@ -105,17 +104,19 @@ public class MusicBotControlHandler {
if (reqVoice(ch, member) == null) return; if (reqVoice(ch, member) == null) return;
lavalinkManager.getMusicManager(guild.getIdLong()).stop(); lavalinkManager.getMusicManager(guild.getIdLong()).stop();
guild.getJDA().getDirectAudioController().disconnect(guild); guild.getJDA().getDirectAudioController().disconnect(guild);
send(ch, ok(0xFF4444, "👋 Отключился", "Вышел из голосового канала.")); send(ch, ok(ERR, "👋 Отключился", "Вышел из голосового канала."));
} }
public void handleRepeat(MessageChannel ch, Guild guild, Member member) { public void handleRepeat(MessageChannel ch, Guild guild, Member member) {
if (reqVoice(ch, member) == null) return; if (reqVoice(ch, member) == null) return;
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler(); var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
boolean now = !s.isRepeating(); var now = !s.isRepeating();
s.setRepeating(now); s.setRepeating(now);
String t = now ? "🔁 Повтор включён" : "➡ Повтор выключен"; send(
String d = now ? "Очередь будет повторяться." : "Повтор отключён."; ch,
send(ch, ok(t, d)); ok(
now ? "🔁 Повтор включён" : "➡ Повтор выключен",
now ? "Очередь будет повторяться." : "Повтор отключён."));
} }
public void handleShuffle(MessageChannel ch, Guild guild, Member member) { public void handleShuffle(MessageChannel ch, Guild guild, Member member) {
@ -131,13 +132,12 @@ public class MusicBotControlHandler {
public void handleBassboost(MessageChannel ch, Guild guild, Member member, List<String> args) { public void handleBassboost(MessageChannel ch, Guild guild, Member member, List<String> args) {
if (reqVoice(ch, member) == null) return; if (reqVoice(ch, member) == null) return;
int level = 10; var level = 10;
if (!args.isEmpty()) { if (!args.isEmpty())
try { try {
level = Math.max(-25, Math.min(25, Integer.parseInt(args.get(0)))); level = Math.max(-25, Math.min(25, Integer.parseInt(args.get(0))));
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
} }
}
lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler().setBassboost(level); lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler().setBassboost(level);
send(ch, ok("🔊 Bassboost", "**" + level + "**")); send(ch, ok("🔊 Bassboost", "**" + level + "**"));
} }
@ -145,7 +145,7 @@ public class MusicBotControlHandler {
public void handleNightcore(MessageChannel ch, Guild guild, Member member) { public void handleNightcore(MessageChannel ch, Guild guild, Member member) {
if (reqVoice(ch, member) == null) return; if (reqVoice(ch, member) == null) return;
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler(); var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
boolean now = !s.isNightcore(); var now = !s.filterState.isNightcore();
s.setNightcore(now); s.setNightcore(now);
send(ch, ok("🎵 Nightcore", now ? "Включён ⚡" : "Выключен")); send(ch, ok("🎵 Nightcore", now ? "Включён ⚡" : "Выключен"));
} }
@ -153,7 +153,7 @@ public class MusicBotControlHandler {
public void handleVaporwave(MessageChannel ch, Guild guild, Member member) { public void handleVaporwave(MessageChannel ch, Guild guild, Member member) {
if (reqVoice(ch, member) == null) return; if (reqVoice(ch, member) == null) return;
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler(); var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
boolean now = !s.isVaporwave(); var now = !s.filterState.isVaporwave();
s.setVaporwave(now); s.setVaporwave(now);
send(ch, ok("🎵 Vaporwave", now ? "Включён 🌊" : "Выключен")); send(ch, ok("🎵 Vaporwave", now ? "Включён 🌊" : "Выключен"));
} }
@ -171,10 +171,12 @@ public class MusicBotControlHandler {
return; return;
} }
try { try {
int band = Math.max(0, Math.min(14, Integer.parseInt(args.get(0)))); var band = Math.max(0, Math.min(14, Integer.parseInt(args.get(0))));
float gain = (float) Math.max(-0.25, Math.min(1.0, Double.parseDouble(args.get(1)))); var gain = (float) Math.max(-0.25, Math.min(1.0, Double.parseDouble(args.get(1))));
var sc = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler(); lavalinkManager
sc.setEqualizerBand(band, gain); .getMusicManager(guild.getIdLong())
.getScheduler()
.setEqualizerBand(band, gain);
send( send(
ch, ch,
ok("🎛 Эквалайзер", "Band **" + band + "** → **" + String.format("%.2f", gain) + "**")); ok("🎛 Эквалайзер", "Band **" + band + "** → **" + String.format("%.2f", gain) + "**"));
@ -202,6 +204,6 @@ public class MusicBotControlHandler {
} }
private static EmbedBuilder err(String msg) { private static EmbedBuilder err(String msg) {
return new EmbedBuilder().setColor(0xFF4444).setDescription("❌ " + msg); return new EmbedBuilder().setColor(ERR).setDescription("❌ " + msg);
} }
} }

View file

@ -23,7 +23,7 @@ public class MusicBotListener extends ListenerAdapter {
QueueRepository queueRepository, QueueRepository queueRepository,
creator.loki.feature.music.core.PlayerManager playerManager) { creator.loki.feature.music.core.PlayerManager playerManager) {
this.playHandler = new MusicBotPlayHandler(lavalinkManager); this.playHandler = new MusicBotPlayHandler(lavalinkManager);
this.controlHandler = new MusicBotControlHandler(lavalinkManager, playerManager); this.controlHandler = new MusicBotControlHandler(lavalinkManager);
} }
@Override @Override

View file

@ -34,34 +34,39 @@ public class FilterState {
return vaporwave; return vaporwave;
} }
public void setBassboost(int level) { public FilterState setBassboost(int level) {
this.bassboost = Math.max(0f, Math.min(1.0f, level / 100.0f)); this.bassboost = Math.max(0f, Math.min(1.0f, level / 100.0f));
return this;
} }
public void setNightcore(boolean enabled) { public FilterState setNightcore(boolean enabled) {
this.nightcore = enabled; this.nightcore = enabled;
if (enabled) this.vaporwave = false; if (enabled) this.vaporwave = false;
return this;
} }
public void setVaporwave(boolean enabled) { public FilterState setVaporwave(boolean enabled) {
this.vaporwave = enabled; this.vaporwave = enabled;
if (enabled) this.nightcore = false; if (enabled) this.nightcore = false;
return this;
} }
public void setEqualizerBand(int band, float gain) { public FilterState setEqualizerBand(int band, float gain) {
if (band < 0 || band > 14) return; if (band < 0 || band > 14) return this;
customBands.removeIf(b -> b.getBand() == band); customBands.removeIf(b -> b.getBand() == band);
customBands.add(new Band(band, Math.max(-0.25f, Math.min(10.0f, gain)))); customBands.add(new Band(band, Math.max(-0.25f, Math.min(10.0f, gain))));
return this;
} }
public void reset() { public FilterState reset() {
this.bassboost = 0f; this.bassboost = 0f;
this.nightcore = false; this.nightcore = false;
this.vaporwave = false; this.vaporwave = false;
this.customBands.clear(); this.customBands.clear();
return this;
} }
public void apply(Optional<Link> link) { public FilterState apply(Optional<Link> link) {
link.ifPresent( link.ifPresent(
l -> { l -> {
FilterBuilder fb = new FilterBuilder(); FilterBuilder fb = new FilterBuilder();
@ -86,5 +91,6 @@ public class FilterState {
.setFilters(fb.build()) .setFilters(fb.build())
.subscribe(null, err -> System.err.println("Filter error: " + err.getMessage())); .subscribe(null, err -> System.err.println("Filter error: " + err.getMessage()));
}); });
return this;
} }
} }

View file

@ -12,6 +12,7 @@ import creator.loki.feature.music.core.LavalinkManager;
import creator.loki.feature.music.repository.QueueRepository; import creator.loki.feature.music.repository.QueueRepository;
import creator.loki.feature.music.repository.QueueRepository.QueueTrack; import creator.loki.feature.music.repository.QueueRepository.QueueTrack;
import creator.loki.util.LoggerDecorator; import creator.loki.util.LoggerDecorator;
import dev.arbjerg.lavalink.client.LavalinkNode;
import dev.arbjerg.lavalink.client.Link; import dev.arbjerg.lavalink.client.Link;
import dev.arbjerg.lavalink.client.player.LavalinkPlayer; import dev.arbjerg.lavalink.client.player.LavalinkPlayer;
import dev.arbjerg.lavalink.client.player.Track; import dev.arbjerg.lavalink.client.player.Track;
@ -27,7 +28,6 @@ public class GuildMusicManager {
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private ScheduledFuture<?> autoLeaveFuture; private ScheduledFuture<?> autoLeaveFuture;
@SuppressWarnings("this-escape")
public GuildMusicManager( public GuildMusicManager(
long guildId, LavalinkManager lavalinkManager, QueueRepository queueRepository) { long guildId, LavalinkManager lavalinkManager, QueueRepository queueRepository) {
this.guildId = guildId; this.guildId = guildId;
@ -62,20 +62,17 @@ public class GuildMusicManager {
public void startAutoLeaveTimer() { public void startAutoLeaveTimer() {
cancelAutoLeaveTimer(); cancelAutoLeaveTimer();
autoLeaveFuture = autoLeaveFuture = executor.schedule(this::autoLeave, 2, TimeUnit.MINUTES);
executor.schedule( }
() -> {
stop(); private void autoLeave() {
getLink() stop();
.ifPresent( getLink()
link -> .ifPresent(
link.destroy() link ->
.subscribe( link.destroy()
null, err -> logger.warn("Lavalink: {}", err.getMessage()))); .subscribe(null, err -> logger.warn("Lavalink: {}", err.getMessage())));
lavalinkManager.removeMusicManager(guildId); lavalinkManager.removeMusicManager(guildId);
},
2,
TimeUnit.MINUTES);
} }
public void cancelAutoLeaveTimer() { public void cancelAutoLeaveTimer() {
@ -101,43 +98,33 @@ public class GuildMusicManager {
return guildId; return guildId;
} }
QueueRepository getQueueRepo() {
return queueRepository;
}
private void loadQueueAsync() { private void loadQueueAsync() {
List<QueueTrack> saved = queueRepository.loadQueue(String.valueOf(guildId)); var saved = queueRepository.loadQueue(String.valueOf(guildId));
if (saved.isEmpty()) return; if (saved.isEmpty()) return;
var client = lavalinkManager.getClient(); var client = lavalinkManager.getClient();
if (client == null || client.getNodes().isEmpty()) return; if (client == null || client.getNodes().isEmpty()) return;
var node = client.getNodes().get(0); executor.execute(() -> restoreTracks(saved, client.getNodes().get(0)));
}
executor.execute( private void restoreTracks(List<QueueTrack> saved, LavalinkNode node) {
() -> { var decoded = new ArrayList<Track>();
List<Track> decoded = new ArrayList<>(); for (var qt : saved) {
for (QueueTrack qt : saved) { try {
try { var track = node.decodeTrack(qt.encoded()).block();
Track track = node.decodeTrack(qt.encoded()).block(); if (track != null) decoded.add(track);
if (track != null) decoded.add(track); } catch (Exception e) {
} catch (Exception e) { LoggerDecorator.warn(logger, "Не удалось декодировать: " + qt.title());
LoggerDecorator.warn(logger, "Не удалось декодировать: " + qt.title()); }
} }
} if (decoded.isEmpty()) return;
if (decoded.isEmpty()) return; var q = scheduler.queue;
var q = scheduler.getRawQueue(); q.addAll(decoded.subList(1, decoded.size()));
q.addAll(decoded.subList(1, decoded.size())); var first = decoded.get(0);
Track first = decoded.get(0); var player = getPlayer().orElse(null);
getPlayer() if (player != null && player.getTrack() == null) scheduler.startTrack(first);
.ifPresentOrElse( else q.offer(first);
player -> { LoggerDecorator.music(logger, "Восстановлено " + decoded.size() + " треков");
if (player.getTrack() == null) scheduler.startTrack(first); queueRepository.deleteQueue(String.valueOf(guildId));
else q.offer(first);
},
() -> q.offer(first));
LoggerDecorator.music(logger, "Восстановлено " + decoded.size() + " треков");
queueRepository.deleteQueue(String.valueOf(guildId));
});
} }
private ScheduledFuture<?> saveDebounced; private ScheduledFuture<?> saveDebounced;
@ -148,20 +135,16 @@ public class GuildMusicManager {
} }
void saveQueue() { void saveQueue() {
Track current = scheduler.getCurrentTrack(); var current = scheduler.getCurrentTrack();
List<Track> tracks = new ArrayList<>(scheduler.getQueue()); var tracks = new ArrayList<>(scheduler.getQueue());
List<QueueTrack> queueTracks = new ArrayList<>(); var queueTracks = new ArrayList<QueueTrack>();
if (current != null) queueTracks.add(toQueueTrack(current)); if (current != null) queueTracks.add(toQueueTrack(current));
for (Track t : tracks) queueTracks.add(toQueueTrack(t)); for (var t : tracks) queueTracks.add(toQueueTrack(t));
queueRepository.saveQueue(String.valueOf(guildId), queueTracks); queueRepository.saveQueue(String.valueOf(guildId), queueTracks);
} }
private static QueueTrack toQueueTrack(Track t) { private static QueueTrack toQueueTrack(Track t) {
return new QueueTrack( var i = t.getInfo();
t.getEncoded(), return new QueueTrack(t.getEncoded(), i.getTitle(), i.getAuthor(), i.getUri(), i.getLength());
t.getInfo().getTitle(),
t.getInfo().getAuthor(),
t.getInfo().getUri(),
t.getInfo().getLength());
} }
} }

View file

@ -6,6 +6,7 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import creator.loki.util.LoggerDecorator; import creator.loki.util.LoggerDecorator;
import dev.arbjerg.lavalink.client.Link; import dev.arbjerg.lavalink.client.Link;
@ -17,45 +18,32 @@ import org.slf4j.LoggerFactory;
public class TrackScheduler { public class TrackScheduler {
private static final Logger log = LoggerFactory.getLogger(TrackScheduler.class); private static final Logger log = LoggerFactory.getLogger(TrackScheduler.class);
private static final Consumer<Throwable> LOG_LAV = t -> log.warn("Lav: {}", t.getMessage());
private final GuildMusicManager manager; private final GuildMusicManager manager;
private final Queue<Track> queue; final Queue<Track> queue = new LinkedList<>();
private boolean repeating; private boolean repeating;
private final FilterState filterState; public final FilterState filterState = new FilterState();
public TrackScheduler(GuildMusicManager manager) { public TrackScheduler(GuildMusicManager manager) {
this.manager = manager; this.manager = manager;
this.queue = new LinkedList<>();
this.repeating = false;
this.filterState = new FilterState();
} }
public void enqueue(Track track) { public void enqueue(Track track) {
manager.cancelAutoLeaveTimer(); manager.cancelAutoLeaveTimer();
manager if (manager.getPlayer().filter(p -> p.getTrack() != null).isPresent()) {
.getPlayer() queue.offer(track);
.ifPresentOrElse( manager.saveQueueDebounced();
player -> { } else {
if (player.getTrack() == null) startTrack(track); startTrack(track);
else { }
queue.offer(track);
manager.saveQueueDebounced();
}
},
() -> startTrack(track));
} }
public void skip() { public void skip() {
manager manager.getPlayer().ifPresent(p -> p.setTrack(null).subscribe(null, LOG_LAV));
.getPlayer()
.ifPresent(
player ->
player
.setTrack(null)
.subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage())));
} }
public boolean nextTrack() { public boolean nextTrack() {
Track next = queue.poll(); var next = queue.poll();
if (next == null) return false; if (next == null) return false;
startTrack(next); startTrack(next);
manager.saveQueueDebounced(); manager.saveQueueDebounced();
@ -65,19 +53,12 @@ public class TrackScheduler {
public void stop() { public void stop() {
manager.cancelAutoLeaveTimer(); manager.cancelAutoLeaveTimer();
queue.clear(); queue.clear();
manager manager.getPlayer().ifPresent(p -> p.setPaused(false).setTrack(null).subscribe(null, LOG_LAV));
.getPlayer()
.ifPresent(
player ->
player
.setPaused(false)
.setTrack(null)
.subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage())));
manager.saveQueue(); manager.saveQueue();
} }
public void shuffle() { public void shuffle() {
List<Track> list = new ArrayList<>(queue); var list = new ArrayList<>(queue);
Collections.shuffle(list); Collections.shuffle(list);
queue.clear(); queue.clear();
queue.addAll(list); queue.addAll(list);
@ -100,103 +81,66 @@ public class TrackScheduler {
this.repeating = r; this.repeating = r;
} }
public boolean isNightcore() {
return filterState.isNightcore();
}
public boolean isVaporwave() {
return filterState.isVaporwave();
}
public void setBassboost(int l) { public void setBassboost(int l) {
filterState.setBassboost(l); filterState.setBassboost(l).apply(manager.getLink());
filterState.apply(manager.getLink());
} }
public void setNightcore(boolean e) { public void setNightcore(boolean e) {
filterState.setNightcore(e); filterState.setNightcore(e).apply(manager.getLink());
filterState.apply(manager.getLink());
} }
public void setVaporwave(boolean e) { public void setVaporwave(boolean e) {
filterState.setVaporwave(e); filterState.setVaporwave(e).apply(manager.getLink());
filterState.apply(manager.getLink());
} }
public void setEqualizerBand(int b, float g) { public void setEqualizerBand(int b, float g) {
filterState.setEqualizerBand(b, g); filterState.setEqualizerBand(b, g).apply(manager.getLink());
filterState.apply(manager.getLink());
} }
public void resetFilters() { public void resetFilters() {
filterState.reset(); filterState.reset().apply(manager.getLink());
filterState.apply(manager.getLink());
}
public void onTrackStart(Track track) {
manager.cancelAutoLeaveTimer();
} }
public void onTrackEnd(Track lastTrack, AudioTrackEndReason endReason) { public void onTrackEnd(Track lastTrack, AudioTrackEndReason endReason) {
if (endReason == AudioTrackEndReason.LOAD_FAILED) { if (endReason == AudioTrackEndReason.LOAD_FAILED) {
LoggerDecorator.warn(log, "Load failed, skipping to next"); LoggerDecorator.warn(log, "Load failed, skipping to next");
Track next = queue.poll(); nextOrAutoLeave();
if (next != null) { } else if (!endReason.getMayStartNext() && endReason != AudioTrackEndReason.FINISHED) {
startTrack(next);
} else {
manager.startAutoLeaveTimer();
}
return;
}
if (!endReason.getMayStartNext() && endReason != AudioTrackEndReason.FINISHED) {
LoggerDecorator.warn(log, "Track ended unexpectedly: " + endReason); LoggerDecorator.warn(log, "Track ended unexpectedly: " + endReason);
manager.startAutoLeaveTimer(); manager.startAutoLeaveTimer();
return; } else if (repeating && endReason == AudioTrackEndReason.FINISHED) {
}
if (repeating && endReason == AudioTrackEndReason.FINISHED) {
startTrack(lastTrack.makeClone()); startTrack(lastTrack.makeClone());
} else { } else {
Track next = queue.poll(); nextOrAutoLeave();
if (next != null) {
startTrack(next);
} else {
manager.startAutoLeaveTimer();
}
} }
} }
Queue<Track> getRawQueue() {
return queue;
}
void startTrack(Track track) { void startTrack(Track track) {
manager.cancelAutoLeaveTimer(); manager.cancelAutoLeaveTimer();
Link link = manager.getLavalinkManager().getOrCreateLink(manager.getGuildId()); var link = manager.getLavalinkManager().getOrCreateLink(manager.getGuildId());
sendTrackDelayed(link, track, 0); sendTrackDelayed(link, track, 0);
} }
private void nextOrAutoLeave() {
var next = queue.poll();
if (next != null) startTrack(next);
else manager.startAutoLeaveTimer();
}
private void scheduleRetry(Runnable r, long delayMs) {
manager.getExecutor().schedule(r, delayMs, TimeUnit.MILLISECONDS);
}
private void sendTrackDelayed(Link link, Track track, int attempt) { private void sendTrackDelayed(Link link, Track track, int attempt) {
if (attempt > 5) return; if (attempt > 5) return;
if (link.getCachedPlayer() == null) { if (link.getCachedPlayer() == null) {
manager scheduleRetry(() -> sendTrackDelayed(link, track, attempt + 1), 300);
.getExecutor()
.schedule(() -> sendTrackDelayed(link, track, attempt + 1), 300, TimeUnit.MILLISECONDS);
return; return;
} }
link.createOrUpdatePlayer() link.createOrUpdatePlayer()
.setTrack(track) .setTrack(track)
.setVolume(100) .setVolume(100)
.subscribe( .subscribe(
null, null, err -> scheduleRetry(() -> sendTrackDelayed(link, track, attempt + 1), 500));
err -> {
if (attempt < 5) {
manager
.getExecutor()
.schedule(
() -> sendTrackDelayed(link, track, attempt + 1),
500,
TimeUnit.MILLISECONDS);
}
});
} }
} }

View file

@ -6,15 +6,19 @@ import java.util.Set;
import creator.loki.command.Command; import creator.loki.command.Command;
import creator.loki.command.CommandContext; import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedTemplates; import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.music.core.LavalinkManager;
import creator.loki.feature.music.core.PlayerManager; import creator.loki.feature.music.core.PlayerManager;
import creator.loki.feature.music.core.SearchManager; import creator.loki.feature.music.core.SearchManager;
import creator.loki.feature.music.manage.GuildMusicManager; import creator.loki.feature.music.manage.GuildMusicManager;
import creator.loki.util.VoiceHelper;
import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler; import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler;
import dev.arbjerg.lavalink.client.player.LoadFailed; import dev.arbjerg.lavalink.client.player.LoadFailed;
import dev.arbjerg.lavalink.client.player.PlaylistLoaded; import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
import dev.arbjerg.lavalink.client.player.SearchResult; import dev.arbjerg.lavalink.client.player.SearchResult;
import dev.arbjerg.lavalink.client.player.Track; import dev.arbjerg.lavalink.client.player.Track;
import dev.arbjerg.lavalink.client.player.TrackLoaded; import dev.arbjerg.lavalink.client.player.TrackLoaded;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
public class PlayCommand implements Command { public class PlayCommand implements Command {
private static final Set<String> SOURCES = Set.of("yt", "sc", "list"); private static final Set<String> SOURCES = Set.of("yt", "sc", "list");
@ -48,9 +52,9 @@ public class PlayCommand implements Command {
@Override @Override
public void execute(CommandContext ctx) { public void execute(CommandContext ctx) {
var guild = ctx.getGuild(); Guild guild = ctx.getGuild();
if (guild == null) return; if (guild == null) return;
var member = ctx.getAuthor(); Member member = ctx.getAuthor();
if (member == null || !member.getVoiceState().inAudioChannel()) { if (member == null || !member.getVoiceState().inAudioChannel()) {
ctx.getChannel() ctx.getChannel()
.sendMessageEmbeds( .sendMessageEmbeds(
@ -60,11 +64,7 @@ public class PlayCommand implements Command {
} }
var args = ctx.getArgs(); var args = ctx.getArgs();
if (args.isEmpty()) { if (args.isEmpty()) {
ctx.getChannel() sendErr(ctx, "Укажи URL, запрос или номер: `" + getUsage() + "`");
.sendMessageEmbeds(
EmbedTemplates.error(
"Аргументы", "Укажи URL, запрос или номер: `" + getUsage() + "`"))
.queue();
return; return;
} }
String first = args.get(0); String first = args.get(0);
@ -78,29 +78,16 @@ public class PlayCommand implements Command {
return; return;
} }
var lm = playerManager.getLavalinkManager(); var lm = playerManager.getLavalinkManager();
if (!guild.getMember(guild.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) { if (!guild.getSelfMember().getVoiceState().inAudioChannel()) {
var vc = member.getVoiceState().getChannel().asVoiceChannel(); var vc = member.getVoiceState().getChannel().asVoiceChannel();
guild.getJDA().getDirectAudioController().connect(vc); guild.getJDA().getDirectAudioController().connect(vc);
if (!guild.getAudioManager().isConnected()) { if (!guild.getAudioManager().isConnected()) guild.getAudioManager().openAudioConnection(vc);
guild.getAudioManager().openAudioConnection(vc); var self = guild.getSelfMember();
} VoiceHelper.waitForCondition(() -> self.getVoiceState().inAudioChannel());
for (int i = 0; i < 10; i++) { if (!self.getVoiceState().inAudioChannel()) return;
if (guild.getMember(guild.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) break;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
return;
}
}
var link = lm.getOrCreateLink(guild.getIdLong()); var link = lm.getOrCreateLink(guild.getIdLong());
for (int i = 0; i < 10; i++) { VoiceHelper.waitForCondition(() -> link.getCachedPlayer() != null);
if (link.getCachedPlayer() != null) break; if (link.getCachedPlayer() == null) return;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
return;
}
}
} }
var mm = lm.getMusicManager(guild.getIdLong()); var mm = lm.getMusicManager(guild.getIdLong());
boolean isSource = SOURCES.contains(first); boolean isSource = SOURCES.contains(first);
@ -109,40 +96,31 @@ public class PlayCommand implements Command {
sendErr(ctx, "Укажи запрос после источника."); sendErr(ctx, "Укажи запрос после источника.");
return; return;
} }
boolean isUrl = raw.startsWith("http://") || raw.startsWith("https://"); boolean isUrl = raw.startsWith("http://") || raw.startsWith("https://");
boolean isList = !isUrl && first.equals("list"); String query = isUrl ? null : (first.equals("list") ? raw : raw);
String id = buildQuery(first, raw);
String id; loadAndHandle(ctx, lm, guild.getIdLong(), mm, id, isUrl, query, raw);
if (isUrl) { }
id = raw;
} else if (isList) {
id = "ytsearch:" + raw;
} else if (raw.startsWith("ytmsearch:")
|| raw.startsWith("ytsearch:")
|| raw.startsWith("scsearch:")) {
id = raw;
} else {
id = "ytsearch:" + raw;
}
boolean autoPlay = isUrl;
private void loadAndHandle(
CommandContext ctx,
LavalinkManager lm,
long guildId,
GuildMusicManager mm,
String id,
boolean isUrl,
String query,
String raw) {
String noMatch = String noMatch =
isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw; isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw;
String query = isList ? raw : (isUrl ? null : raw); lm.getOrCreateLink(guildId)
lm.getOrCreateLink(guild.getIdLong())
.loadItem(id) .loadItem(id)
.subscribe( .subscribe(
new AbstractAudioLoadResultHandler() { new AbstractAudioLoadResultHandler() {
@Override @Override
public void ontrackLoaded(TrackLoaded r) { public void ontrackLoaded(TrackLoaded r) {
if (autoPlay) { if (isUrl) playTrack(ctx, r.getTrack());
playTrack(ctx, r.getTrack()); else showResults(ctx, query, mm, List.of(r.getTrack()));
} else {
showResults(ctx, query, mm, List.of(r.getTrack()));
}
} }
@Override @Override
@ -159,7 +137,7 @@ public class PlayCommand implements Command {
"Плейлист добавлен", "Плейлист добавлен",
"**" + r.getInfo().getName() + "** — " + c + " треков")) "**" + r.getInfo().getName() + "** — " + c + " треков"))
.queue(); .queue();
} else if (autoPlay && r.getTracks().size() <= 1 && !r.getTracks().isEmpty()) { } else if (r.getTracks().size() <= 1 && !r.getTracks().isEmpty()) {
playTrack(ctx, r.getTracks().get(0)); playTrack(ctx, r.getTracks().get(0));
} else { } else {
showResults(ctx, query, mm, r.getTracks()); showResults(ctx, query, mm, r.getTracks());
@ -170,7 +148,7 @@ public class PlayCommand implements Command {
public void onSearchResultLoaded(SearchResult r) { public void onSearchResultLoaded(SearchResult r) {
if (r.getTracks().isEmpty()) { if (r.getTracks().isEmpty()) {
sendErr(ctx, noMatch); sendErr(ctx, noMatch);
} else if (autoPlay) { } else if (isUrl) {
playTrack(ctx, r.getTracks().get(0)); playTrack(ctx, r.getTracks().get(0));
} else { } else {
showResults(ctx, query, mm, r.getTracks()); showResults(ctx, query, mm, r.getTracks());
@ -193,6 +171,16 @@ public class PlayCommand implements Command {
}); });
} }
private static String buildQuery(String first, String raw) {
if (raw.startsWith("http://") || raw.startsWith("https://")) return raw;
if (!first.equals("list")
&& !raw.startsWith("ytsearch:")
&& !raw.startsWith("ytmsearch:")
&& !raw.startsWith("scsearch:")) return "ytsearch:" + raw;
if (first.equals("list")) return "ytsearch:" + raw;
return raw;
}
private void showResults(CommandContext ctx, String q, GuildMusicManager m, List<Track> list) { private void showResults(CommandContext ctx, String q, GuildMusicManager m, List<Track> list) {
if (q == null) return; if (q == null) return;
searchHandler.storeResults(ctx.getGuild().getIdLong(), list); searchHandler.storeResults(ctx.getGuild().getIdLong(), list);

View file

@ -28,7 +28,7 @@ public class FilterSlashHandler {
public void handleNightcore(SlashCommandInteractionEvent event) { public void handleNightcore(SlashCommandInteractionEvent event) {
if (reqVoice(event) == null) return; if (reqVoice(event) == null) return;
var manager = getManager(event); var manager = getManager(event);
boolean now = !manager.getScheduler().isNightcore(); boolean now = !manager.getScheduler().filterState.isNightcore();
manager.getScheduler().setNightcore(now); manager.getScheduler().setNightcore(now);
event event
.replyEmbeds( .replyEmbeds(
@ -39,7 +39,7 @@ public class FilterSlashHandler {
public void handleVaporwave(SlashCommandInteractionEvent event) { public void handleVaporwave(SlashCommandInteractionEvent event) {
if (reqVoice(event) == null) return; if (reqVoice(event) == null) return;
var manager = getManager(event); var manager = getManager(event);
boolean now = !manager.getScheduler().isVaporwave(); boolean now = !manager.getScheduler().filterState.isVaporwave();
manager.getScheduler().setVaporwave(now); manager.getScheduler().setVaporwave(now);
event event
.replyEmbeds( .replyEmbeds(

View file

@ -2,193 +2,144 @@ package creator.loki.feature.music.slash;
import java.awt.Color; import java.awt.Color;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
import creator.loki.embed.EmbedFactory; import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates; import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.music.core.PlayerManager; import creator.loki.feature.music.core.PlayerManager;
import creator.loki.feature.music.manage.GuildMusicManager;
import dev.arbjerg.lavalink.client.player.Track; import dev.arbjerg.lavalink.client.player.Track;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class QueueSlashHandler { public class QueueSlashHandler {
private final PlayerManager playerManager; private final PlayerManager pm;
public QueueSlashHandler(PlayerManager playerManager) { public QueueSlashHandler(PlayerManager pm) {
this.playerManager = playerManager; this.pm = pm;
} }
public void handleStop(SlashCommandInteractionEvent event) { public void handleStop(SlashCommandInteractionEvent ev) {
if (reqVoice(event) == null) return; if (reqVoice(ev) == null) return;
var manager = getManager(event); pm.getMusicManager(ev.getGuild().getIdLong()).stop();
manager.stop(); ev.getGuild().getJDA().getDirectAudioController().disconnect(ev.getGuild());
event.getGuild().getJDA().getDirectAudioController().disconnect(event.getGuild()); ev.replyEmbeds(EmbedTemplates.info("Остановлено", "Музыка остановлена, очередь очищена."))
event
.replyEmbeds(EmbedTemplates.info("Остановлено", "Музыка остановлена, очередь очищена."))
.queue(); .queue();
} }
public void handleSkip(SlashCommandInteractionEvent event) { public void handleSkip(SlashCommandInteractionEvent ev) {
if (reqVoice(event) == null) return; if (reqVoice(ev) == null) return;
var manager = getManager(event); var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
if (manager.getScheduler().getCurrentTrack() == null) { if (s.getCurrentTrack() == null) {
event.replyEmbeds(EmbedTemplates.error("Очередь пуста", "Сейчас ничего не играет.")).queue(); ev.replyEmbeds(EmbedTemplates.error("Очередь пуста", "Сейчас ничего не играет.")).queue();
return; return;
} }
String skipped = manager.getScheduler().getCurrentTrack().getInfo().getTitle(); var t = s.getCurrentTrack().getInfo().getTitle();
manager.getScheduler().skip(); s.skip();
event.replyEmbeds(EmbedTemplates.info("Пропущено", "**" + skipped + "**")).queue(); ev.replyEmbeds(EmbedTemplates.info("Пропущено", "**" + t + "**")).queue();
} }
public void handleQueue(SlashCommandInteractionEvent event) { public void handleQueue(SlashCommandInteractionEvent ev) {
var manager = getManager(event); var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
Track current = manager.getScheduler().getCurrentTrack(); var cur = s.getCurrentTrack();
List<Track> queue = manager.getScheduler().getQueue(); var q = s.getQueue();
if (current == null && queue.isEmpty()) { if (cur == null && q.isEmpty()) {
event.reply("Очередь пуста.").queue(); ev.reply("Очередь пуста.").queue();
return; return;
} }
var embed = var eb = EmbedFactory.create().withTitle("Очередь треков").withColor(new Color(88, 101, 242));
EmbedFactory.create().withTitle("Очередь треков").withColor(new Color(88, 101, 242)); if (cur != null) {
if (current != null) { var ci = cur.getInfo();
embed.addField( eb.addField("Сейчас играет", "**" + ci.getTitle() + "**\n" + ci.getAuthor(), false);
"Сейчас играет",
"**" + current.getInfo().getTitle() + "**\n" + current.getInfo().getAuthor(),
false);
} }
if (!queue.isEmpty()) { if (!q.isEmpty()) eb.addField("В очереди (" + q.size() + ")", buildQueueList(q), false);
StringBuilder sb = new StringBuilder(); ev.replyEmbeds(eb.build()).queue();
int show = Math.min(queue.size(), 10);
for (int i = 0; i < show; i++) {
Track t = queue.get(i);
sb.append("`").append(i + 1).append(".` **").append(t.getInfo().getTitle()).append("**\n");
}
if (queue.size() > show) {
sb.append("... и ещё **").append(queue.size() - show).append("** треков");
}
embed.addField("В очереди (" + queue.size() + ")", sb.toString(), false);
}
event.replyEmbeds(embed.build()).queue();
} }
public void handleNowPlaying(SlashCommandInteractionEvent event) { private String buildQueueList(List<Track> q) {
var manager = getManager(event); var sb = new StringBuilder();
var track = manager.getScheduler().getCurrentTrack(); var show = Math.min(q.size(), 10);
for (int i = 0; i < show; i++) {
var t = q.get(i);
sb.append("`").append(i + 1).append(".` **").append(t.getInfo().getTitle()).append("**\n");
}
if (q.size() > show) sb.append("... и ещё **").append(q.size() - show).append("** треков");
return sb.toString();
}
public void handleNowPlaying(SlashCommandInteractionEvent ev) {
var track = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler().getCurrentTrack();
if (track == null) { if (track == null) {
event.reply("Сейчас ничего не играет.").queue(); ev.reply("Сейчас ничего не играет.").queue();
return; return;
} }
var link = var link = pm.getLavalinkManager().getClient().getLinkIfCached(ev.getGuild().getIdLong());
playerManager var pos =
.getLavalinkManager() link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getPosition() : 0L;
.getClient() var eb =
.getLinkIfCached(event.getGuild().getIdLong()); EmbedFactory.create()
long pos = .withTitle("Сейчас играет")
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getPosition() : 0; .withColor(new Color(88, 101, 242))
long dur = track.getInfo().getLength(); .addField("Название", "**" + track.getInfo().getTitle() + "**", false)
event .addField("Автор", track.getInfo().getAuthor(), true)
.replyEmbeds( .addField("Длительность", progressBar(pos, track.getInfo().getLength()), true)
EmbedFactory.create() .build();
.withTitle("Сейчас играет") ev.replyEmbeds(eb).queue();
.withColor(new Color(88, 101, 242))
.addField("Название", "**" + track.getInfo().getTitle() + "**", false)
.addField("Автор", track.getInfo().getAuthor(), true)
.addField("Длительность", progressBar(pos, dur), true)
.build())
.queue();
} }
public void handleVolume(SlashCommandInteractionEvent event) { public void handleVolume(SlashCommandInteractionEvent ev) {
if (reqVoice(event) == null) return; if (reqVoice(ev) == null) return;
var link = var link = pm.getLavalinkManager().getClient().getLinkIfCached(ev.getGuild().getIdLong());
playerManager var opt = ev.getOption("percent");
.getLavalinkManager() if (opt == null) {
.getClient() var cur =
.getLinkIfCached(event.getGuild().getIdLong());
Integer percent =
event.getOption("percent") != null ? event.getOption("percent").getAsInt() : null;
if (percent == null) {
int vol =
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getVolume() : 100; link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getVolume() : 100;
event.replyEmbeds(EmbedTemplates.info("Громкость", "Текущая: **" + vol + "%**")).queue(); ev.replyEmbeds(EmbedTemplates.info("Громкость", "Текущая: **" + cur + "%**")).queue();
return; return;
} }
int vol = Math.max(0, Math.min(100, percent)); var vol = Math.max(0, Math.min(100, opt.getAsInt()));
if (link != null && link.getCachedPlayer() != null) { if (link != null && link.getCachedPlayer() != null)
link.createOrUpdatePlayer() link.createOrUpdatePlayer().setVolume(vol).subscribe(null, LOG);
.setVolume(vol) ev.replyEmbeds(EmbedTemplates.info("Громкость", "Установлена на **" + vol + "%**")).queue();
.subscribe(
null,
err ->
LoggerFactory.getLogger(QueueSlashHandler.class)
.warn("Lavalink: {}", err.getMessage()));
}
event.replyEmbeds(EmbedTemplates.info("Громкость", "Установлена на **" + vol + "%**")).queue();
} }
public void handlePause(SlashCommandInteractionEvent event) { public void handlePause(SlashCommandInteractionEvent ev) {
if (reqVoice(event) == null) return; if (reqVoice(ev) == null) return;
getManager(event) pm.getMusicManager(ev.getGuild().getIdLong())
.getPlayer() .getPlayer()
.ifPresent( .ifPresent(pl -> pl.setPaused(true).subscribe(null, LOG));
p -> ev.replyEmbeds(EmbedTemplates.info("Пауза", "Музыка поставлена на паузу.")).queue();
p.setPaused(true)
.subscribe(
null,
err ->
LoggerFactory.getLogger(QueueSlashHandler.class)
.warn("Lavalink: {}", err.getMessage())));
event.replyEmbeds(EmbedTemplates.info("Пауза", "Музыка поставлена на паузу.")).queue();
} }
public void handleResume(SlashCommandInteractionEvent event) { public void handleResume(SlashCommandInteractionEvent ev) {
if (reqVoice(event) == null) return; if (reqVoice(ev) == null) return;
getManager(event) pm.getMusicManager(ev.getGuild().getIdLong())
.getPlayer() .getPlayer()
.ifPresent( .ifPresent(pl -> pl.setPaused(false).subscribe(null, LOG));
p -> ev.replyEmbeds(EmbedTemplates.info("Продолжаем", "Воспроизведение продолжено.")).queue();
p.setPaused(false)
.subscribe(
null,
err ->
LoggerFactory.getLogger(QueueSlashHandler.class)
.warn("Lavalink: {}", err.getMessage())));
event.replyEmbeds(EmbedTemplates.info("Продолжаем", "Воспроизведение продолжено.")).queue();
} }
public void handleShuffle(SlashCommandInteractionEvent event) { public void handleShuffle(SlashCommandInteractionEvent ev) {
if (reqVoice(event) == null) return; if (reqVoice(ev) == null) return;
getManager(event).getScheduler().shuffle(); pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler().shuffle();
event.replyEmbeds(EmbedTemplates.info("Перемешано", "Очередь перемешана.")).queue(); ev.replyEmbeds(EmbedTemplates.info("Перемешано", "Очередь перемешана.")).queue();
} }
public void handleRepeat(SlashCommandInteractionEvent event) { public void handleRepeat(SlashCommandInteractionEvent ev) {
if (reqVoice(event) == null) return; if (reqVoice(ev) == null) return;
var s = getManager(event).getScheduler(); var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
boolean now = !s.isRepeating(); var now = !s.isRepeating();
s.setRepeating(now); s.setRepeating(now);
event var d = now ? "Повтор очереди включён." : "Повтор очереди выключен.";
.replyEmbeds( ev.replyEmbeds(EmbedTemplates.info("Повтор", d)).queue();
EmbedTemplates.info(
"Повтор", now ? "Повтор очереди включён." : "Повтор очереди выключен."))
.queue();
} }
private Member reqVoice(SlashCommandInteractionEvent event) { private Member reqVoice(SlashCommandInteractionEvent ev) {
Member member = event.getMember(); var m = ev.getMember();
if (member == null || !member.getVoiceState().inAudioChannel()) { if (m != null && m.getVoiceState().inAudioChannel()) return m;
event var em = EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала.");
.replyEmbeds(EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала.")) ev.replyEmbeds(em).setEphemeral(true).queue();
.setEphemeral(true) return null;
.queue();
return null;
}
return member;
}
private GuildMusicManager getManager(SlashCommandInteractionEvent event) {
return playerManager.getMusicManager(event.getGuild().getIdLong());
} }
static String progressBar(long pos, long dur) { static String progressBar(long pos, long dur) {
@ -197,11 +148,12 @@ public class QueueSlashHandler {
int filled = (int) ((double) pos / dur * total); int filled = (int) ((double) pos / dur * total);
var bar = new StringBuilder("\u2503"); var bar = new StringBuilder("\u2503");
for (int i = 0; i < total; i++) bar.append(i < filled ? "\u2588" : "\u2591"); for (int i = 0; i < total; i++) bar.append(i < filled ? "\u2588" : "\u2591");
return bar + "\u2503 " + formatTime(pos) + " / " + formatTime(dur); var p = String.format("%d:%02d", pos / 60000, (pos / 1000) % 60);
var d = String.format("%d:%02d", dur / 60000, (dur / 1000) % 60);
return bar + "\u2503 " + p + " / " + d;
} }
private static String formatTime(long millis) { private static final Consumer<Throwable> LOG =
long s = millis / 1000; err ->
return String.format("%d:%02d", s / 60, s % 60); LoggerFactory.getLogger(QueueSlashHandler.class).warn("Lavalink: {}", err.getMessage());
}
} }

View file

@ -0,0 +1,27 @@
package creator.loki.feature.profile;
import java.awt.Color;
public final class RankCardConstants {
public static final int W = 1400;
public static final int H = 500;
public static final int PAD = 28;
public static final int AVATAR_SIZE = 190;
public static final int AVATAR_X = PAD + 18;
public static final int AVATAR_Y = PAD + 30;
public static final int CONTENT_X = AVATAR_X + AVATAR_SIZE + 40;
public static final int CONTENT_W = W - PAD - 20 - CONTENT_X;
public static final int STATS_Y = 360;
public static final int STAT_H = 100;
public static final int STAT_GAP = 18;
public static final Color ACCENT = new Color(255, 145, 0);
public static final Color ACCENT_DIM = new Color(255, 145, 0, 60);
public static final Color WHITE = Color.WHITE;
public static final Color GREY = new Color(190, 190, 200);
public static final Color DARK_CARD = new Color(12, 12, 18, 200);
public static final Color OVERLAY = new Color(8, 8, 16, 180);
public static final Color BAR_BG = new Color(255, 255, 255, 30);
private RankCardConstants() {}
}

View file

@ -1,158 +1,140 @@
package creator.loki.feature.profile; package creator.loki.feature.profile;
import static creator.loki.feature.profile.RankCardConstants.*;
import java.awt.BasicStroke; import java.awt.BasicStroke;
import java.awt.Color; import java.awt.Color;
import java.awt.Font; import java.awt.FontMetrics;
import java.awt.FontFormatException; import java.awt.GradientPaint;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Ellipse2D; import java.awt.geom.Ellipse2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
public class RankCardGenerator { public class RankCardGenerator {
private static final int WIDTH = 1400; private final RankCardRenderer renderer = new RankCardRenderer();
private static final int HEIGHT = 500;
private static final Map<String, Font> FONT_CACHE = new ConcurrentHashMap<>();
@SuppressWarnings("deprecation")
public BufferedImage generate(ProfileData data) throws Exception { public BufferedImage generate(ProfileData data) throws Exception {
BufferedImage canvas = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB); BufferedImage canvas = new BufferedImage(W, H, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = canvas.createGraphics(); Graphics2D g = canvas.createGraphics();
RankCardRenderer.applyHints(g);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.drawImage(renderer.loadBackground("backgrounds/" + data.getBackground()), 0, 0, W, H, null);
g.setRenderingHint( g.setColor(OVERLAY);
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g.fillRect(0, 0, W, H);
drawCard(g, PAD, PAD, W - PAD * 2, H - PAD * 2);
BufferedImage bg = loadBackground("backgrounds/" + data.getBackground()); drawRankBadge(g, data.getRank());
g.drawImage(bg, 0, 0, WIDTH, HEIGHT, null);
g.setColor(new Color(0, 0, 0, 170));
g.fillRoundRect(20, 20, WIDTH - 40, HEIGHT - 40, 40, 40);
g.setStroke(new BasicStroke(2f));
g.setColor(new Color(255, 140, 0, 100));
g.drawRoundRect(20, 20, WIDTH - 40, HEIGHT - 40, 40, 40);
BufferedImage avatar = ImageIO.read(new URI(data.getAvatarUrl()).toURL()); BufferedImage avatar = ImageIO.read(new URI(data.getAvatarUrl()).toURL());
drawCircleImage(g, avatar, 70, 90, 180); drawAvatar(g, avatar, AVATAR_X, AVATAR_Y, AVATAR_SIZE);
g.setStroke(new BasicStroke(5f)); int nameY = AVATAR_Y + 58;
g.setColor(new Color(255, 140, 0)); g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 50f));
g.drawOval(70, 90, 180, 180); g.setColor(WHITE);
g.drawString(data.getUsername(), CONTENT_X, nameY);
int nameW = g.getFontMetrics().stringWidth(data.getUsername());
drawLevelTag(g, data.getLevel(), CONTENT_X + nameW + 20, nameY - 40);
g.setFont(loadFont("fonts/Inter-Bold.ttf", 52f)); g.setFont(renderer.loadFont("fonts/Inter-Regular.ttf", 26f));
g.setColor(Color.WHITE); g.setColor(GREY);
g.drawString(data.getUsername(), 320, 150); g.drawString(data.getSubtitle(), CONTENT_X, nameY + 36);
g.setFont(loadFont("fonts/Inter-Bold.ttf", 24f)); int barLabelY = nameY + 100;
g.setColor(new Color(255, 140, 0)); g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 22f));
g.fillRoundRect(670, 110, 90, 42, 15, 15); g.setColor(GREY);
g.setColor(Color.BLACK); g.drawString("ОПЫТ", CONTENT_X, barLabelY);
g.drawString(data.getTag(), 690, 138); String xpStr = data.getXp() + " / " + data.getMaxXp() + " XP";
g.setColor(ACCENT);
FontMetrics fm = g.getFontMetrics();
g.drawString(xpStr, CONTENT_X + CONTENT_W - fm.stringWidth(xpStr), barLabelY);
renderer.drawXpBar(g, CONTENT_X, barLabelY + 12, CONTENT_W, 22, data.getXp(), data.getMaxXp());
g.setFont(loadFont("fonts/Inter-Regular.ttf", 28f)); int statW = (CONTENT_W - STAT_GAP * 2) / 3;
g.setColor(new Color(220, 220, 220)); renderer.drawStatCard(
g.drawString(data.getSubtitle(), 320, 210); g,
"АКТИВНОСТЬ",
g.setFont(loadFont("fonts/Inter-Bold.ttf", 48f)); data.getActivity(),
g.setColor(new Color(255, 140, 0)); RankCardIcons.StatIcon.CLOCK,
g.drawString(String.valueOf(data.getLevel()), 320, 300); CONTENT_X,
g.setFont(loadFont("fonts/Inter-Bold.ttf", 36f)); STATS_Y,
g.drawString("УРОВЕНЬ", 380, 300); statW,
STAT_H);
g.setFont(loadFont("fonts/Inter-Regular.ttf", 34f)); renderer.drawStatCard(
g.setColor(Color.WHITE); g,
g.drawString(data.getXp() + " / " + data.getMaxXp() + " XP", 920, 300); "СООБЩЕНИЙ",
String.valueOf(data.getMessages()),
int barX = 320; RankCardIcons.StatIcon.MESSAGE,
int barY = 340; CONTENT_X + statW + STAT_GAP,
int barWidth = 620; STATS_Y,
int barHeight = 28; statW,
g.setColor(new Color(255, 255, 255, 40)); STAT_H);
g.fillRoundRect(barX, barY, barWidth, barHeight, 20, 20); renderer.drawStatCard(
g,
float progress = (float) data.getXp() / data.getMaxXp(); "РЕПУТАЦИЯ",
int fill = (int) (barWidth * progress); String.valueOf(data.getReputation()),
g.setColor(new Color(255, 140, 0)); RankCardIcons.StatIcon.STAR,
g.fillRoundRect(barX, barY, fill, barHeight, 20, 20); CONTENT_X + (statW + STAT_GAP) * 2,
STATS_Y,
int dotX = barX + fill - 10; statW,
g.setColor(Color.WHITE); STAT_H);
g.fillOval(dotX, barY - 6, 40, 40);
drawIconStat(g, "АКТИВНОСТЬ", data.getActivity(), IconType.CLOCK, 80, 400);
drawIconStat(g, "СООБЩЕНИЙ", String.valueOf(data.getMessages()), IconType.MESSAGE, 500, 400);
drawIconStat(g, "РЕПУТАЦИЯ", String.valueOf(data.getReputation()), IconType.STAR, 920, 400);
g.dispose(); g.dispose();
return canvas; return canvas;
} }
private enum IconType { private void drawCard(Graphics2D g, int x, int y, int w, int h) {
CLOCK, RoundRectangle2D rect = new RoundRectangle2D.Float(x, y, w, h, 36, 36);
MESSAGE, g.setColor(new Color(10, 10, 18, 210));
STAR g.fill(rect);
GradientPaint gp = new GradientPaint(x, y, ACCENT_DIM, x + 6, y, new Color(0, 0, 0, 0));
g.setPaint(gp);
g.setStroke(new BasicStroke(3f));
g.draw(rect);
g.setPaint(new Color(255, 145, 0, 80));
g.setStroke(new BasicStroke(1.5f));
g.draw(rect);
} }
private void drawIconStat(Graphics2D g, String title, String value, IconType icon, int x, int y) { private void drawAvatar(Graphics2D g, BufferedImage src, int x, int y, int size) {
g.setColor(new Color(15, 15, 15, 180)); g.setColor(new Color(255, 145, 0, 45));
g.fillRoundRect(x, y, 340, 110, 30, 30); g.fillOval(x - 10, y - 10, size + 20, size + 20);
Ellipse2D clip = new Ellipse2D.Double(x, y, size, size);
g.setColor(new Color(255, 140, 0)); g.setClip(clip);
g.fillOval(x + 25, y + 25, 48, 48); g.drawImage(src, x, y, size, size, null);
g.setColor(Color.WHITE);
g.setFont(loadFont("fonts/Inter-Bold.ttf", 36f));
g.setColor(Color.WHITE);
g.drawString(value, x + 100, y + 50);
g.setFont(loadFont("fonts/Inter-Regular.ttf", 24f));
g.setColor(new Color(180, 180, 180));
g.drawString(title, x + 100, y + 90);
}
private void drawCircleImage(Graphics2D g, BufferedImage image, int x, int y, int size) {
Ellipse2D.Double circle = new Ellipse2D.Double(x, y, size, size);
g.setClip(circle);
g.drawImage(image, x, y, size, size, null);
g.setClip(null); g.setClip(null);
g.setStroke(new BasicStroke(4f));
g.setColor(ACCENT);
g.drawOval(x, y, size, size);
} }
private Font loadFont(String path, float size) { private void drawLevelTag(Graphics2D g, int level, int x, int y) {
String key = path + "@" + size; String text = "LVL " + level;
Font cached = FONT_CACHE.get(key); g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 20f));
if (cached != null) { FontMetrics fm = g.getFontMetrics();
return cached; int tw = fm.stringWidth(text);
} int th = fm.getAscent();
try (InputStream is = getClass().getClassLoader().getResourceAsStream("assets/" + path)) { int px = 14, py = 8;
if (is == null) { g.setColor(ACCENT);
Font fallback = new Font("Arial", Font.PLAIN, (int) size); g.fillRoundRect(x, y, tw + px * 2, th + py * 2, 12, 12);
FONT_CACHE.put(key, fallback); g.setColor(new Color(20, 10, 0));
return fallback; g.drawString(text, x + px, y + py + th - 2);
}
Font font = Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(size);
FONT_CACHE.put(key, font);
return font;
} catch (FontFormatException | IOException e) {
Font fallback = new Font("Arial", Font.PLAIN, (int) size);
FONT_CACHE.put(key, fallback);
return fallback;
}
} }
private BufferedImage loadBackground(String path) throws Exception { private void drawRankBadge(Graphics2D g, int rank) {
try (InputStream is = getClass().getClassLoader().getResourceAsStream("assets/" + path)) { int bx = W - PAD - 20 - 110;
if (is == null) { int by = PAD + 22;
throw new IllegalArgumentException("Resource not found: assets/" + path); g.setColor(new Color(20, 20, 30, 200));
} g.fillRoundRect(bx, by, 110, 44, 14, 14);
return ImageIO.read(is); g.setStroke(new BasicStroke(1.2f));
} g.setColor(ACCENT_DIM);
g.drawRoundRect(bx, by, 110, 44, 14, 14);
g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 14f));
g.setColor(GREY);
g.drawString("РАНГ", bx + 14, by + 17);
g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 26f));
g.setColor(ACCENT);
g.drawString("#" + rank, bx + 14, by + 37);
} }
} }

View file

@ -0,0 +1,58 @@
package creator.loki.feature.profile;
import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.geom.GeneralPath;
public final class RankCardIcons {
public enum StatIcon {
CLOCK,
MESSAGE,
STAR
}
public static void draw(Graphics2D g, StatIcon icon, int cx, int cy, int r) {
g.setStroke(new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
switch (icon) {
case CLOCK -> drawClock(g, cx, cy, r);
case MESSAGE -> drawMessage(g, cx, cy, r);
case STAR -> drawStar(g, cx, cy, r);
}
}
private static void drawClock(Graphics2D g, int cx, int cy, int r) {
g.drawOval(cx - r, cy - r, r * 2, r * 2);
g.drawLine(cx, cy, cx - r / 2, cy - r / 2);
g.drawLine(cx, cy, cx, cy - (int) (r * 0.8));
g.fillOval(cx - 2, cy - 2, 4, 4);
}
private static void drawMessage(Graphics2D g, int cx, int cy, int r) {
int bx = cx - r, by = cy - (int) (r * 0.85);
int bw = r * 2, bh = (int) (r * 1.5);
g.drawRoundRect(bx, by, bw, bh, 6, 6);
GeneralPath tail = new GeneralPath();
tail.moveTo(cx - r / 2, by + bh);
tail.lineTo(cx - r / 2 - 4, by + bh + 7);
tail.lineTo(cx - r / 2 + 8, by + bh);
tail.closePath();
g.fill(tail);
}
private static void drawStar(Graphics2D g, int cx, int cy, int r) {
double inner = r * 0.42;
GeneralPath star = new GeneralPath();
for (int i = 0; i < 10; i++) {
double angle = Math.PI / 5 * i - Math.PI / 2;
double rad = (i % 2 == 0) ? r : inner;
double px = cx + Math.cos(angle) * rad;
double py = cy + Math.sin(angle) * rad;
if (i == 0) star.moveTo(px, py);
else star.lineTo(px, py);
}
star.closePath();
g.fill(star);
}
private RankCardIcons() {}
}

View file

@ -0,0 +1,118 @@
package creator.loki.feature.profile;
import static creator.loki.feature.profile.RankCardConstants.*;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.imageio.ImageIO;
public class RankCardRenderer {
private static final Map<String, Font> FONT_CACHE = new ConcurrentHashMap<>();
public void drawXpBar(Graphics2D g, int x, int y, int w, int h, int xp, int maxXp) {
g.setColor(BAR_BG);
g.fillRoundRect(x, y, w, h, h, h);
float pct = maxXp > 0 ? Math.min(1f, (float) xp / maxXp) : 0f;
int fill = Math.max(h, (int) (w * pct));
GradientPaint gp = new GradientPaint(x, y, new Color(255, 100, 0), x + fill, y, ACCENT);
g.setPaint(gp);
g.fillRoundRect(x, y, fill, h, h, h);
int dotD = h + 14;
int dotX = x + fill - dotD / 2;
int dotY = y - 7;
g.setColor(new Color(30, 20, 0));
g.fillOval(dotX, dotY, dotD, dotD);
g.setColor(ACCENT);
g.setStroke(new BasicStroke(2.5f));
g.drawOval(dotX, dotY, dotD, dotD);
g.setColor(WHITE);
g.fillOval(dotX + 5, dotY + 5, dotD - 10, dotD - 10);
}
public void drawStatCard(
Graphics2D g,
String title,
String value,
RankCardIcons.StatIcon icon,
int x,
int y,
int w,
int h) {
g.setColor(DARK_CARD);
g.fillRoundRect(x, y, w, h, 20, 20);
g.setStroke(new BasicStroke(1f));
g.setColor(new Color(255, 145, 0, 40));
g.drawRoundRect(x, y, w, h, 20, 20);
int iconD = 48;
int iconX = x + 18;
int iconY = y + (h - iconD) / 2;
g.setColor(new Color(255, 145, 0, 30));
g.fillOval(iconX, iconY, iconD, iconD);
g.setColor(ACCENT);
g.setStroke(new BasicStroke(1.5f));
RankCardIcons.draw(g, icon, iconX + iconD / 2, iconY + iconD / 2, 13);
int textX = iconX + iconD + 16;
g.setFont(loadFont("fonts/Inter-Bold.ttf", 30f));
g.setColor(WHITE);
g.drawString(value, textX, y + 46);
g.setFont(loadFont("fonts/Inter-Regular.ttf", 18f));
g.setColor(GREY);
g.drawString(title, textX, y + 72);
}
public Font loadFont(String path, float size) {
String key = path + "@" + size;
Font cached = FONT_CACHE.get(key);
if (cached != null) return cached;
try (InputStream is = getClass().getClassLoader().getResourceAsStream("assets/" + path)) {
if (is == null) {
Font fallback = new Font("SansSerif", Font.BOLD, (int) size);
FONT_CACHE.put(key, fallback);
return fallback;
}
Font font = Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(size);
FONT_CACHE.put(key, font);
return font;
} catch (FontFormatException | IOException e) {
Font fallback = new Font("SansSerif", Font.BOLD, (int) size);
FONT_CACHE.put(key, fallback);
return fallback;
}
}
public BufferedImage loadBackground(String path) throws Exception {
try (InputStream is = getClass().getClassLoader().getResourceAsStream("assets/" + path)) {
if (is == null) {
BufferedImage fallback = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB);
Graphics2D fg = fallback.createGraphics();
fg.setColor(new Color(15, 15, 25));
fg.fillRect(0, 0, W, H);
fg.dispose();
return fallback;
}
return ImageIO.read(is);
}
}
public static void applyHints(Graphics2D g) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setRenderingHint(
RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.setRenderingHint(
RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
}
}

View file

@ -2,10 +2,15 @@ package creator.loki.feature.radio;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.feature.music.core.LavalinkManager; import creator.loki.feature.music.core.LavalinkManager;
import creator.loki.feature.music.manage.GuildMusicManager; import creator.loki.feature.music.manage.GuildMusicManager;
import creator.loki.feature.music.repository.QueueRepository;
import creator.loki.util.LoggerDecorator; import creator.loki.util.LoggerDecorator;
import creator.loki.util.VoiceHelper;
import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler; import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler;
import dev.arbjerg.lavalink.client.Link; import dev.arbjerg.lavalink.client.Link;
import dev.arbjerg.lavalink.client.player.TrackLoaded; import dev.arbjerg.lavalink.client.player.TrackLoaded;
@ -14,23 +19,31 @@ import net.dv8tion.jda.api.entities.Guild;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@Singleton
public class RadioManager { public class RadioManager {
private static final Logger logger = LoggerFactory.getLogger(RadioManager.class); private static final Logger logger = LoggerFactory.getLogger(RadioManager.class);
private static final long VOICE_TIMEOUT_MS = 5000;
private final Map<Long, GuildRadioState> radioStates = new ConcurrentHashMap<>(); private final Map<Long, GuildRadioState> radioStates = new ConcurrentHashMap<>();
private final QueueRepository queueRepository;
private LavalinkManager lavalinkManager; private LavalinkManager lavalinkManager;
private JDA radioJda; private JDA radioJda;
public void setLavalinkManager(LavalinkManager lm) { @Inject
this.lavalinkManager = lm; public RadioManager(QueueRepository queueRepository) {
this.queueRepository = queueRepository;
} }
public void setRadioJda(JDA radioJda) { public void setLavalinkManager(LavalinkManager lm) { this.lavalinkManager = lm; }
this.radioJda = radioJda;
} public void setRadioJda(JDA radioJda) { this.radioJda = radioJda; }
public void setRadio(long guildId, String streamUrl) { public void setRadio(long guildId, String streamUrl) {
radioStates.computeIfAbsent(guildId, id -> new GuildRadioState()).streamUrl = streamUrl; radioStates.compute(guildId, (id, state) -> {
if (state == null) state = new GuildRadioState();
state.streamUrl = streamUrl;
return state;
});
LoggerDecorator.music(logger, "Radio URL set for guild " + guildId + ": " + streamUrl); LoggerDecorator.music(logger, "Radio URL set for guild " + guildId + ": " + streamUrl);
} }
@ -43,86 +56,65 @@ public class RadioManager {
GuildRadioState state = radioStates.get(guildId); GuildRadioState state = radioStates.get(guildId);
if (state == null || state.streamUrl == null || state.streamUrl.isBlank()) return; if (state == null || state.streamUrl == null || state.streamUrl.isBlank()) return;
if (radioJda == null || lavalinkManager == null) return; if (radioJda == null || lavalinkManager == null) return;
if (isRadioActive(guildId) && state.musicManager != null) {
stopRadio(guildId);
}
var finalState = radioStates.computeIfAbsent(guildId, id -> new GuildRadioState());
finalState.streamUrl = getRadioUrl(guildId);
Guild guild = radioJda.getGuildById(guildId); Guild guild = radioJda.getGuildById(guildId);
if (guild == null) return; if (guild == null) return;
guild.getJDA().getDirectAudioController().connect(guild.getVoiceChannelById(channelId));
guild.getJDA().getDirectAudioController().connect( var self = guild.getMember(guild.getJDA().getSelfUser());
guild.getVoiceChannelById(channelId)); if (!VoiceHelper.waitForCondition(() -> self.getVoiceState().inAudioChannel(), VOICE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
LoggerDecorator.warn(logger, "Radio: voice connection timeout for guild " + guildId);
for (int i = 0; i < 10; i++) { return;
if (guild.getMember(guild.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) break;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
return;
}
} }
var link = lavalinkManager.getOrCreateLink(guildId); var link = lavalinkManager.getOrCreateLink(guildId);
for (int i = 0; i < 10; i++) { if (!VoiceHelper.waitForCondition(() -> link.getCachedPlayer() != null, VOICE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
if (link.getCachedPlayer() != null) break; LoggerDecorator.warn(logger, "Radio: Lavalink player timeout for guild " + guildId);
try { return;
Thread.sleep(200);
} catch (InterruptedException e) {
return;
}
} }
var mm = new GuildMusicManager(guildId, lavalinkManager, queueRepository);
var mm = new GuildMusicManager(guildId, lavalinkManager, null); int vol = finalState.volume;
state.musicManager = mm; var url = finalState.streamUrl;
finalState.musicManager = mm;
lavalinkManager lavalinkManager.getOrCreateLink(guildId).loadItem(url)
.getOrCreateLink(guildId) .subscribe(new AbstractAudioLoadResultHandler() {
.loadItem(state.streamUrl) @Override
.subscribe( public void ontrackLoaded(TrackLoaded r) {
new AbstractAudioLoadResultHandler() { if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe();
@Override mm.getScheduler().enqueue(r.getTrack());
public void ontrackLoaded(TrackLoaded r) { LoggerDecorator.music(logger, "Radio started in guild " + guildId);
mm.getScheduler().enqueue(r.getTrack()); }
LoggerDecorator.music(logger, "Radio started in guild " + guildId); @Override
} public void onPlaylistLoaded(dev.arbjerg.lavalink.client.player.PlaylistLoaded r) {
if (!r.getTracks().isEmpty()) {
@Override if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe();
public void onPlaylistLoaded( mm.getScheduler().enqueue(r.getTracks().get(0));
dev.arbjerg.lavalink.client.player.PlaylistLoaded r) { LoggerDecorator.music(logger, "Radio started in guild " + guildId);
if (!r.getTracks().isEmpty()) { }
mm.getScheduler().enqueue(r.getTracks().get(0)); }
LoggerDecorator.music(logger, "Radio started in guild " + guildId); @Override
} public void onSearchResultLoaded(dev.arbjerg.lavalink.client.player.SearchResult r) {
} if (!r.getTracks().isEmpty()) {
if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe();
@Override mm.getScheduler().enqueue(r.getTracks().get(0));
public void onSearchResultLoaded( LoggerDecorator.music(logger, "Radio started in guild " + guildId);
dev.arbjerg.lavalink.client.player.SearchResult r) { }
if (!r.getTracks().isEmpty()) { }
mm.getScheduler().enqueue(r.getTracks().get(0)); @Override
LoggerDecorator.music(logger, "Radio started in guild " + guildId); public void noMatches() { LoggerDecorator.warn(logger, "No radio stream: " + url); }
} @Override
} public void loadFailed(dev.arbjerg.lavalink.client.player.LoadFailed e) {
LoggerDecorator.warn(logger, "Failed to load radio: " + (e.getException().getMessage() != null ? e.getException().getMessage() : "unknown"));
@Override }
public void noMatches() { });
LoggerDecorator.warn(logger, "No radio stream: " + state.streamUrl);
}
@Override
public void loadFailed(
dev.arbjerg.lavalink.client.player.LoadFailed e) {
LoggerDecorator.warn(logger, "Failed to load radio: "
+ (e.getException().getMessage() != null
? e.getException().getMessage() : "unknown"));
}
});
} }
public void stopRadio(long guildId) { public void stopRadio(long guildId) {
GuildRadioState state = radioStates.remove(guildId); GuildRadioState state = radioStates.remove(guildId);
if (state == null) return; if (state == null) return;
if (state.musicManager != null) { if (state.musicManager != null) { state.musicManager.stop(); state.musicManager.destroy(); }
state.musicManager.stop();
state.musicManager.destroy();
}
if (lavalinkManager != null) { if (lavalinkManager != null) {
Link link = lavalinkManager.getClient().getLinkIfCached(guildId); Link link = lavalinkManager.getClient().getLinkIfCached(guildId);
if (link != null) link.destroy().subscribe(); if (link != null) link.destroy().subscribe();
@ -135,16 +127,13 @@ public class RadioManager {
} }
public boolean isRadioActive(long guildId) { public boolean isRadioActive(long guildId) {
return radioStates.containsKey(guildId); GuildRadioState state = radioStates.get(guildId);
return state != null && state.streamUrl != null;
} }
public boolean hasRadioBot() { public boolean hasRadioBot() { return radioJda != null; }
return radioJda != null;
}
public JDA getRadioJda() { public JDA getRadioJda() { return radioJda; }
return radioJda;
}
public int getVolume(long guildId) { public int getVolume(long guildId) {
GuildRadioState state = radioStates.get(guildId); GuildRadioState state = radioStates.get(guildId);
@ -152,13 +141,13 @@ public class RadioManager {
} }
public void setVolume(long guildId, int volume) { public void setVolume(long guildId, int volume) {
GuildRadioState state = radioStates.computeIfAbsent(guildId, id -> new GuildRadioState()); GuildRadioState state = radioStates.get(guildId);
if (state == null) return;
state.volume = Math.max(0, Math.min(100, volume)); state.volume = Math.max(0, Math.min(100, volume));
if (lavalinkManager != null) { if (lavalinkManager != null) {
var link = lavalinkManager.getClient().getLinkIfCached(guildId); var link = lavalinkManager.getClient().getLinkIfCached(guildId);
if (link != null && link.getCachedPlayer() != null) { if (link != null && link.getCachedPlayer() != null) {
link.createOrUpdatePlayer() link.createOrUpdatePlayer().setVolume(state.volume)
.setVolume(state.volume)
.subscribe(null, err -> logger.warn("Lavalink: {}", err.getMessage())); .subscribe(null, err -> logger.warn("Lavalink: {}", err.getMessage()));
} }
} }

View file

@ -8,15 +8,16 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import creator.loki.feature.counter.DailyStats; import creator.loki.feature.counter.DailyStats;
import creator.loki.feature.counter.repository.CounterStatsRepository; import creator.loki.feature.counter.repository.CounterStatsRepository;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
public class ServerStatsService { public class ServerStatsService {
private static final int CHART_WIDTH = 800; private static final int CHART_WIDTH = 800;
private static final int CHART_HEIGHT = 400; private static final int CHART_HEIGHT = 400;
private static final int MARGIN = 60; private static final int MARGIN = 60;
@ -38,62 +39,56 @@ public class ServerStatsService {
} }
public byte[] generateChart(List<DailyStats> history) throws Exception { public byte[] generateChart(List<DailyStats> history) throws Exception {
BufferedImage image = new BufferedImage(CHART_WIDTH, CHART_HEIGHT, BufferedImage.TYPE_INT_RGB); var image = new BufferedImage(CHART_WIDTH, CHART_HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics(); var g = image.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint( g.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setColor(COLOR_BG); g.setColor(COLOR_BG);
g.fillRect(0, 0, CHART_WIDTH, CHART_HEIGHT); g.fillRect(0, 0, CHART_WIDTH, CHART_HEIGHT);
var maxVal =
int maxVal =
history.stream().mapToInt(s -> Math.max(s.getJoined(), s.getLeft())).max().orElse(1); history.stream().mapToInt(s -> Math.max(s.getJoined(), s.getLeft())).max().orElse(1);
var plotW = CHART_WIDTH - 2 * MARGIN;
int plotW = CHART_WIDTH - 2 * MARGIN; var plotH = CHART_HEIGHT - 2 * MARGIN;
int plotH = CHART_HEIGHT - 2 * MARGIN; var barW = Math.max(4, plotW / (history.size() * 2 + 1));
int barW = Math.max(4, plotW / (history.size() * 2 + 1));
g.setColor(COLOR_GRID); g.setColor(COLOR_GRID);
for (int i = 0; i <= 4; i++) { for (var i = 0; i <= 4; i++) {
int y = MARGIN + plotH - (plotH * i / 4); var y = MARGIN + plotH - (plotH * i / 4);
g.drawLine(MARGIN, y, CHART_WIDTH - MARGIN, y); g.drawLine(MARGIN, y, CHART_WIDTH - MARGIN, y);
} }
drawLabels(g, plotH, maxVal);
Font labelFont = new Font("Arial", Font.PLAIN, 11); var maxBars = Math.min(history.size(), 30);
Font titleFont = new Font("Arial", Font.BOLD, 16);
g.setColor(COLOR_TEXT);
g.setFont(titleFont);
g.drawString("Активность сервера (присоединились / вышли)", 160, 30);
g.setFont(labelFont);
g.setColor(COLOR_TEXT);
for (int i = 0; i <= 4; i++) {
int y = MARGIN + plotH - (plotH * i / 4);
String label = String.valueOf(maxVal * i / 4);
g.drawString(label, 5, y + 4);
}
int maxBars = Math.min(history.size(), 30);
history = history.subList(history.size() - maxBars, history.size()); history = history.subList(history.size() - maxBars, history.size());
drawData(g, history, maxVal, plotH, barW);
drawLegend(g);
g.dispose();
var baos = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", baos);
return baos.toByteArray();
}
int step = Math.max(1, history.size() / 15); private void drawLabels(Graphics2D g, int plotH, int maxVal) {
g.setColor(COLOR_TEXT);
for (int i = 0; i < history.size(); i++) { g.setFont(new Font("Arial", Font.BOLD, 16));
DailyStats stats = history.get(i); g.drawString("Активность сервера (присоединились / вышли)", 160, 30);
int x = MARGIN + i * (barW * 2 + 2); g.setFont(new Font("Arial", Font.PLAIN, 11));
for (var i = 0; i <= 4; i++) {
int joinedH = (int) ((double) stats.getJoined() / maxVal * plotH); var y = MARGIN + plotH - (plotH * i / 4);
int leftH = (int) ((double) stats.getLeft() / maxVal * plotH); g.drawString(String.valueOf(maxVal * i / 4), 5, y + 4);
}
}
private void drawData(Graphics2D g, List<DailyStats> history, int maxVal, int plotH, int barW) {
var step = Math.max(1, history.size() / 15);
for (var i = 0; i < history.size(); i++) {
var stats = history.get(i);
var x = MARGIN + i * (barW * 2 + 2);
var joinedH = (int) ((double) stats.getJoined() / maxVal * plotH);
var leftH = (int) ((double) stats.getLeft() / maxVal * plotH);
g.setColor(COLOR_JOINED); g.setColor(COLOR_JOINED);
g.fillRect(x, MARGIN + plotH - joinedH, barW, joinedH); g.fillRect(x, MARGIN + plotH - joinedH, barW, joinedH);
g.setColor(COLOR_LEFT); g.setColor(COLOR_LEFT);
g.fillRect(x + barW + 2, MARGIN + plotH - leftH, barW, leftH); g.fillRect(x + barW + 2, MARGIN + plotH - leftH, barW, leftH);
if (i % step == 0) { if (i % step == 0) {
g.setColor(COLOR_TEXT); g.setColor(COLOR_TEXT);
g.rotate(-Math.PI / 4, x + barW, CHART_HEIGHT - MARGIN + 15); g.rotate(-Math.PI / 4, x + barW, CHART_HEIGHT - MARGIN + 15);
@ -101,60 +96,48 @@ public class ServerStatsService {
g.rotate(Math.PI / 4, x + barW, CHART_HEIGHT - MARGIN + 15); g.rotate(Math.PI / 4, x + barW, CHART_HEIGHT - MARGIN + 15);
} }
} }
}
private void drawLegend(Graphics2D g) {
g.setFont(new Font("Arial", Font.PLAIN, 12)); g.setFont(new Font("Arial", Font.PLAIN, 12));
g.setColor(COLOR_JOINED); g.setColor(COLOR_JOINED);
g.fillRect(CHART_WIDTH - 170, 10, 12, 12); g.fillRect(CHART_WIDTH - 170, 10, 12, 12);
g.setColor(COLOR_TEXT); g.setColor(COLOR_TEXT);
g.drawString("Присоединились", CHART_WIDTH - 152, 21); g.drawString("Присоединились", CHART_WIDTH - 152, 21);
g.setColor(COLOR_LEFT); g.setColor(COLOR_LEFT);
g.fillRect(CHART_WIDTH - 60, 10, 12, 12); g.fillRect(CHART_WIDTH - 60, 10, 12, 12);
g.setColor(COLOR_TEXT); g.setColor(COLOR_TEXT);
g.drawString("Вышли", CHART_WIDTH - 42, 21); g.drawString("Вышли", CHART_WIDTH - 42, 21);
g.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", baos);
return baos.toByteArray();
} }
public byte[] generateCsv(Guild guild, List<DailyStats> history) { public byte[] generateCsv(Guild guild, List<DailyStats> history) {
String header = var members = guild.getMembers();
var owner = guild.getOwner();
var online = members.stream().filter(m -> m.getOnlineStatus() == OnlineStatus.ONLINE).count();
var bots = members.stream().filter(m -> m.getUser().isBot()).count();
var info = new StringBuilder();
info.append(escapeCsv(guild.getName())).append(",");
info.append(guild.getId()).append(",");
info.append(owner != null ? escapeCsv(owner.getUser().getGlobalName()) : "").append(",");
info.append(guild.getTimeCreated().format(DateTimeFormatter.ISO_DATE)).append(",");
info.append(guild.getMemberCount()).append(",");
info.append(online).append(",");
info.append(bots).append(",");
info.append(guild.getTextChannels().size()).append(",");
info.append(guild.getVoiceChannels().size()).append(",");
info.append(guild.getRoles().size()).append("\n");
var dailyRows = new StringBuilder();
for (var s : history) {
dailyRows.append(
s.getDate() + "," + s.getJoined() + "," + s.getLeft() + "," + s.getDifference() + "\n");
}
var totalJoined = history.stream().mapToInt(DailyStats::getJoined).sum();
var totalLeft = history.stream().mapToInt(DailyStats::getLeft).sum();
var totals = String.format("%ntotal,%d,%d,%d", totalJoined, totalLeft, totalJoined - totalLeft);
var header =
"server_name,server_id,owner,created_at,total_members,online,bots,text_channels,voice_channels,roles\n"; "server_name,server_id,owner,created_at,total_members,online,bots,text_channels,voice_channels,roles\n";
String info = return (header + info + "\ndate,joined,left,difference\n" + dailyRows + totals)
String.format( .getBytes(java.nio.charset.StandardCharsets.UTF_8);
"%s,%s,%s,%s,%d,%d,%d,%d,%d,%d%n",
escapeCsv(guild.getName()),
guild.getId(),
guild.getOwner() != null ? escapeCsv(guild.getOwner().getUser().getGlobalName()) : "",
guild.getTimeCreated().format(DateTimeFormatter.ISO_DATE),
guild.getMemberCount(),
guild.getMembers().stream()
.filter(m -> m.getOnlineStatus() == net.dv8tion.jda.api.OnlineStatus.ONLINE)
.count(),
guild.getMembers().stream().filter(m -> m.getUser().isBot()).count(),
guild.getTextChannels().size(),
guild.getVoiceChannels().size(),
guild.getRoles().size());
String dailyHeader = "\ndate,joined,left,difference\n";
String dailyRows =
history.stream()
.map(
s ->
String.format(
"%s,%d,%d,%d", s.getDate(), s.getJoined(), s.getLeft(), s.getDifference()))
.collect(Collectors.joining("\n"));
int totalJoined = history.stream().mapToInt(DailyStats::getJoined).sum();
int totalLeft = history.stream().mapToInt(DailyStats::getLeft).sum();
String totals =
String.format("%ntotal,%d,%d,%d", totalJoined, totalLeft, totalJoined - totalLeft);
String csv = header + info + dailyHeader + dailyRows + totals;
return csv.getBytes(java.nio.charset.StandardCharsets.UTF_8);
} }
private static String escapeCsv(String value) { private static String escapeCsv(String value) {

View file

@ -1,84 +0,0 @@
package creator.loki.feature.social.commands;
import java.awt.Color;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandCategories;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import creator.loki.integration.nekos.NekosLifeApi;
import net.dv8tion.jda.api.entities.Member;
/** Обниматься с пользователем с GIF */
public class CuddleCommand implements Command {
private final CooldownManager cooldownManager;
public CuddleCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "cuddle";
}
@Override
public String getDescription() {
return "Обниматься с пользователем";
}
@Override
public String getUsage() {
return "cuddle <@user>";
}
@Override
public String getCategory() {
return CommandCategories.GIF_INTERACTIVE;
}
@Override
public void execute(CommandContext context) {
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "cuddle")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "cuddle");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
cooldownManager.applyCooldown(context.getAuthor().getId(), "cuddle", 5);
String gifUrl = NekosLifeApi.fetchImage("cuddle");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withDescription(
"🥰 "
+ context.getAuthor().getAsMention()
+ " обнимается с "
+ target.getAsMention()
+ "! 💕")
.withImage(gifUrl)
.withColor(new Color(255, 0, 255))
.withFooter("Powered by nekos.life")
.build())
.queue();
}
}

View file

@ -1,84 +0,0 @@
package creator.loki.feature.social.commands;
import java.awt.Color;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandCategories;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import creator.loki.integration.nekos.NekosLifeApi;
import net.dv8tion.jda.api.entities.Member;
/** Покормить пользователя с GIF */
public class FeedCommand implements Command {
private final CooldownManager cooldownManager;
public FeedCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "feed";
}
@Override
public String getDescription() {
return "Покормить пользователя";
}
@Override
public String getUsage() {
return "feed <@user>";
}
@Override
public String getCategory() {
return CommandCategories.GIF_INTERACTIVE;
}
@Override
public void execute(CommandContext context) {
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "feed")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "feed");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
cooldownManager.applyCooldown(context.getAuthor().getId(), "feed", 5);
String gifUrl = NekosLifeApi.fetchImage("feed");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withDescription(
"🍽️ "
+ context.getAuthor().getAsMention()
+ " кормит "
+ target.getAsMention()
+ "! 😋")
.withImage(gifUrl)
.withColor(new Color(255, 200, 100))
.withFooter("Powered by nekos.life")
.build())
.queue();
}
}

View file

@ -1,69 +0,0 @@
package creator.loki.feature.social.commands;
import java.awt.Color;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandCategories;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.integration.nekos.NekosLifeApi;
import net.dv8tion.jda.api.entities.Member;
public class HugCommand implements Command {
@Override
public String getName() {
return "hug";
}
@Override
public String getDescription() {
return "Обнять пользователя";
}
@Override
public String getUsage() {
return "hug <@user>";
}
@Override
public String getCategory() {
return CommandCategories.GIF_INTERACTIVE;
}
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
// Получаем GIF из nekos.life API
String gifUrl = NekosLifeApi.fetchImage("hug");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withDescription(
"🤗 "
+ context.getAuthor().getAsMention()
+ " обнял "
+ target.getAsMention()
+ " 💕")
.withImage(gifUrl)
.withColor(new Color(255, 182, 193))
.withFooter("Powered by nekos.life")
.build())
.queue();
}
}

View file

@ -1,101 +0,0 @@
package creator.loki.feature.social.commands;
import java.awt.Color;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandCategories;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import creator.loki.integration.nekos.NekosLifeApi;
import net.dv8tion.jda.api.entities.Member;
/** Поцелуй пользователя с GIF */
public class KissCommand implements Command {
private final CooldownManager cooldownManager;
public KissCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "kiss";
}
@Override
public String getDescription() {
return "Поцеловать пользователя";
}
@Override
public String getUsage() {
return "kiss <@user>";
}
@Override
public String getCategory() {
return CommandCategories.GIF_INTERACTIVE;
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "kiss")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "kiss");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context
.getChannel()
.sendMessage("❌ Укажи кого поцеловать! Использование: `!kiss @user`")
.queue();
return;
}
Member target = mentioned.get(0);
if (target.getId().equals(context.getAuthor().getId())) {
context.getChannel().sendMessage("❌ Нельзя целовать самого себя!").queue();
return;
}
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не умеют целоваться!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "kiss", 5);
// Получаем GIF из nekos.life API
String gifUrl = NekosLifeApi.fetchImage("kiss");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withDescription(
"💋 "
+ context.getAuthor().getAsMention()
+ " целует "
+ target.getAsMention()
+ " ❤️")
.withImage(gifUrl)
.withColor(new Color(255, 105, 180))
.withFooter("Powered by nekos.life")
.build())
.queue();
}
}

View file

@ -1,84 +0,0 @@
package creator.loki.feature.social.commands;
import java.awt.Color;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandCategories;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import creator.loki.integration.nekos.NekosLifeApi;
import net.dv8tion.jda.api.entities.Member;
/** Тыкнуть пользователя с GIF */
public class PokeCommand implements Command {
private final CooldownManager cooldownManager;
public PokeCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "poke";
}
@Override
public String getDescription() {
return "Тыкнуть пользователя";
}
@Override
public String getUsage() {
return "poke <@user>";
}
@Override
public String getCategory() {
return CommandCategories.GIF_INTERACTIVE;
}
@Override
public void execute(CommandContext context) {
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "poke")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "poke");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
cooldownManager.applyCooldown(context.getAuthor().getId(), "poke", 5);
String gifUrl = NekosLifeApi.fetchImage("poke");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withDescription(
"👉 "
+ context.getAuthor().getAsMention()
+ " тыкает в "
+ target.getAsMention()
+ "! 😏")
.withImage(gifUrl)
.withColor(new Color(0, 128, 128))
.withFooter("Powered by nekos.life")
.build())
.queue();
}
}

View file

@ -1,84 +0,0 @@
package creator.loki.feature.social.commands;
import java.awt.Color;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandCategories;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import creator.loki.integration.nekos.NekosLifeApi;
import net.dv8tion.jda.api.entities.Member;
/** Шлепнуть пользователя с GIF */
public class SlapCommand implements Command {
private final CooldownManager cooldownManager;
public SlapCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "slap";
}
@Override
public String getDescription() {
return "Шлепнуть пользователя";
}
@Override
public String getUsage() {
return "slap <@user>";
}
@Override
public String getCategory() {
return CommandCategories.GIF_INTERACTIVE;
}
@Override
public void execute(CommandContext context) {
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "slap")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "slap");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
cooldownManager.applyCooldown(context.getAuthor().getId(), "slap", 5);
String gifUrl = NekosLifeApi.fetchImage("slap");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withDescription(
"👋 "
+ context.getAuthor().getAsMention()
+ " шлепает "
+ target.getAsMention()
+ "! 😠")
.withImage(gifUrl)
.withColor(new Color(255, 165, 0))
.withFooter("Powered by nekos.life")
.build())
.queue();
}
}

View file

@ -0,0 +1,140 @@
package creator.loki.feature.social.commands;
import java.awt.Color;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandCategories;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import creator.loki.integration.nekos.NekosLifeApi;
import net.dv8tion.jda.api.entities.Member;
public class SocialCommand implements Command {
public enum Flavor {
HUG("hug", "Обнять пользователя", "🤗", "обнял", new Color(255, 182, 193), false, false),
KISS("kiss", "Поцеловать пользователя", "💋", "целует", new Color(255, 105, 180), true, true),
SLAP("slap", "Шлепнуть пользователя", "👋", "шлепает", new Color(255, 165, 0), false, false),
POKE("poke", "Тыкнуть пользователя", "👉", "тыкает в", new Color(0, 128, 128), false, false),
TICKLE(
"tickle", "Пощекотать пользователя", "🤗", "щекочет", new Color(0, 255, 0), false, false),
CUDDLE(
"cuddle",
"Обниматься с пользователем",
"🥰",
"обнимается с",
new Color(255, 0, 255),
false,
false),
FEED("feed", "Покормить пользователя", "🍽️", "кормит", new Color(255, 200, 100), false, false);
final String name;
final String description;
final String emoji;
final String verb;
final Color color;
final boolean checkSelf;
final boolean checkBot;
Flavor(
String name,
String description,
String emoji,
String verb,
Color color,
boolean checkSelf,
boolean checkBot) {
this.name = name;
this.description = description;
this.emoji = emoji;
this.verb = verb;
this.color = color;
this.checkSelf = checkSelf;
this.checkBot = checkBot;
}
}
private final Flavor flavor;
private final CooldownManager cooldownManager;
public SocialCommand(Flavor flavor, CooldownManager cooldownManager) {
this.flavor = flavor;
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return flavor.name;
}
@Override
public String getDescription() {
return flavor.description;
}
@Override
public String getUsage() {
return flavor.name + " <@user>";
}
@Override
public String getCategory() {
return CommandCategories.GIF_INTERACTIVE;
}
@Override
public void execute(CommandContext context) {
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), flavor.name)) {
long remaining =
cooldownManager.getRemainingSeconds(context.getAuthor().getId(), flavor.name);
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
if (flavor.checkSelf && target.getId().equals(context.getAuthor().getId())) {
context.getChannel().sendMessage("❌ Нельзя использовать на себе!").queue();
return;
}
if (flavor.checkBot && target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не поддерживают это действие!").queue();
return;
}
cooldownManager.applyCooldown(context.getAuthor().getId(), flavor.name, 5);
String gifUrl = NekosLifeApi.fetchImage(flavor.name);
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withDescription(
flavor.emoji
+ " "
+ context.getAuthor().getAsMention()
+ " "
+ flavor.verb
+ " "
+ target.getAsMention())
.withImage(gifUrl)
.withColor(flavor.color)
.withFooter("Powered by nekos.life")
.build())
.queue();
}
}

View file

@ -1,84 +0,0 @@
package creator.loki.feature.social.commands;
import java.awt.Color;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandCategories;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import creator.loki.integration.nekos.NekosLifeApi;
import net.dv8tion.jda.api.entities.Member;
/** Пощекотать пользователя с GIF */
public class TickleCommand implements Command {
private final CooldownManager cooldownManager;
public TickleCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "tickle";
}
@Override
public String getDescription() {
return "Пощекотать пользователя";
}
@Override
public String getUsage() {
return "tickle <@user>";
}
@Override
public String getCategory() {
return CommandCategories.GIF_INTERACTIVE;
}
@Override
public void execute(CommandContext context) {
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "tickle")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "tickle");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
cooldownManager.applyCooldown(context.getAuthor().getId(), "tickle", 5);
String gifUrl = NekosLifeApi.fetchImage("tickle");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withDescription(
"🤗 "
+ context.getAuthor().getAsMention()
+ " щекочет "
+ target.getAsMention()
+ "!")
.withImage(gifUrl)
.withColor(new Color(0, 255, 0))
.withFooter("Powered by nekos.life")
.build())
.queue();
}
}

View file

@ -4,13 +4,13 @@ import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import creator.loki.util.LoggerDecorator; import creator.loki.util.LoggerDecorator;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.components.label.Label;
import net.dv8tion.jda.api.components.textinput.TextInput;
import net.dv8tion.jda.api.components.textinput.TextInputStyle;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.components.label.Label;
import net.dv8tion.jda.api.components.textinput.TextInput;
import net.dv8tion.jda.api.components.textinput.TextInputStyle;
import net.dv8tion.jda.api.modals.Modal; import net.dv8tion.jda.api.modals.Modal;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;

View file

@ -9,19 +9,22 @@ import creator.loki.feature.voice.repository.VoicePresetRepository;
import creator.loki.util.LoggerDecorator; import creator.loki.util.LoggerDecorator;
import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.components.actionrow.ActionRow;
import net.dv8tion.jda.api.components.buttons.Button;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.components.actionrow.ActionRow;
import net.dv8tion.jda.api.components.buttons.Button;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class VoiceChannelManager { public class VoiceChannelManager {
private static final Logger logger = LoggerFactory.getLogger(VoiceChannelManager.class); private static final Logger logger = LoggerFactory.getLogger(VoiceChannelManager.class);
private static final EnumSet<Permission> OWNER_PERMS =
EnumSet.of(
Permission.MANAGE_CHANNEL, Permission.VOICE_CONNECT,
Permission.VOICE_MOVE_OTHERS, Permission.VOICE_MUTE_OTHERS);
private final VoiceChannelRepository channelRepo; private final VoiceChannelRepository channelRepo;
private final VoicePresetRepository presetRepo; private final VoicePresetRepository presetRepo;
private String triggerChannelId; private String triggerChannelId;
private String categoryId; private String categoryId;
@ -41,56 +44,36 @@ public class VoiceChannelManager {
public VoiceChannel createPersonalChannel(Member member, Guild guild) { public VoiceChannel createPersonalChannel(Member member, Guild guild) {
try { try {
String channelName = "Канал: " + member.getEffectiveName(); var channel = buildPersonalChannel(member, guild);
channelRepo.saveChannelData(new VoiceChannelData(channel.getId(), member.getId()));
VoiceChannel voiceChannel =
guild
.createVoiceChannel(channelName)
.setParent(guild.getCategoryById(categoryId))
.addPermissionOverride(
member,
EnumSet.of(
Permission.MANAGE_CHANNEL,
Permission.VOICE_CONNECT,
Permission.VOICE_MOVE_OTHERS,
Permission.VOICE_MUTE_OTHERS),
null)
.complete();
channelRepo.saveChannelData(new VoiceChannelData(voiceChannel.getId(), member.getId()));
LoggerDecorator.voice( LoggerDecorator.voice(
logger, logger,
"Создан личный канал " "Создан личный канал %s для пользователя %s"
+ voiceChannel.getId() .formatted(channel.getId(), member.getEffectiveName()));
+ " для пользователя " applyUserPreset(channel, member.getId());
+ member.getEffectiveName()); return channel;
applyUserPreset(voiceChannel, member.getId());
return voiceChannel;
} catch (Exception e) { } catch (Exception e) {
LoggerDecorator.error(logger, "Ошибка при создании личного канала", e); LoggerDecorator.error(logger, "Ошибка при создании личного канала", e);
return null; return null;
} }
} }
private VoiceChannel buildPersonalChannel(Member member, Guild guild) {
return guild
.createVoiceChannel("Канал: " + member.getEffectiveName())
.setParent(guild.getCategoryById(categoryId))
.addPermissionOverride(member, OWNER_PERMS, null)
.complete();
}
public void deletePersonalChannel(String channelId, Guild guild) { public void deletePersonalChannel(String channelId, Guild guild) {
channelRepo.deleteChannelData(channelId);
try { try {
VoiceChannel channel = guild.getVoiceChannelById(channelId); var channel = guild.getVoiceChannelById(channelId);
if (channel != null) { if (channel != null) {
channel channel
.delete() .delete()
.queue( .queue(success -> LoggerDecorator.voice(logger, "Удален личный канал " + channelId));
success -> {
channelRepo.deleteChannelData(channelId);
LoggerDecorator.voice(logger, "Удален личный канал " + channelId);
},
error ->
LoggerDecorator.error(
logger, "Ошибка при удалении канала " + channelId, error));
} else {
channelRepo.deleteChannelData(channelId);
} }
} catch (Exception e) { } catch (Exception e) {
LoggerDecorator.error(logger, "Ошибка при удалении личного канала", e); LoggerDecorator.error(logger, "Ошибка при удалении личного канала", e);
@ -110,43 +93,32 @@ public class VoiceChannelManager {
} }
public void updatePresetSetting(String userId, String setting, Object value) { public void updatePresetSetting(String userId, String setting, Object value) {
VoicePreset preset = getUserPreset(userId); var preset = getUserPreset(userId);
switch (setting) { if ("name".equals(setting)) preset.setName((String) value);
case "name" -> preset.setName((String) value); else if ("limit".equals(setting)) preset.setLimit((Integer) value);
case "limit" -> preset.setLimit((Integer) value); else if ("bitrate".equals(setting)) preset.setBitrate((Integer) value);
case "bitrate" -> preset.setBitrate((Integer) value); else if ("locked".equals(setting)) preset.setLocked((Boolean) value);
case "locked" -> preset.setLocked((Boolean) value);
default -> {
/* ignore */
}
}
saveUserPreset(userId, preset); saveUserPreset(userId, preset);
} }
public void sendControlPanel(VoiceChannel channel, Member owner) { public void sendControlPanel(VoiceChannel channel, Member owner) {
EmbedBuilder embed = var limitText = channel.getUserLimit() == 0 ? "∞" : String.valueOf(channel.getUserLimit());
var embed =
new EmbedBuilder() new EmbedBuilder()
.setTitle("🎛️ Управление каналом") .setTitle("🎛️ Управление каналом")
.setDescription("Ваш личный голосовой канал") .setDescription("Ваш личный голосовой канал")
.addField("Владелец", owner.getAsMention(), true) .addField("Владелец", owner.getAsMention(), true)
.addField( .addField("Лимит", limitText, true)
"Лимит",
channel.getUserLimit() == 0 ? "∞" : String.valueOf(channel.getUserLimit()),
true)
.setColor(Color.CYAN) .setColor(Color.CYAN)
.setFooter("Управляйте каналом с помощью кнопок ниже"); .setFooter("Управляйте каналом с помощью кнопок ниже");
var row =
String cid = channel.getId(); ActionRow.of(
Button lockBtn = Button.primary("voice_lock:" + cid, "🔒 Замок"); Button.primary("voice_lock:" + channel.getId(), "🔒 Замок"),
Button limitBtn = Button.secondary("voice_limit:" + cid, "👥 Лимит"); Button.secondary("voice_limit:" + channel.getId(), "👥 Лимит"),
Button renameBtn = Button.secondary("voice_rename:" + cid, "✏️ Название"); Button.secondary("voice_rename:" + channel.getId(), "✏️ Название"),
Button bitrateBtn = Button.secondary("voice_bitrate:" + cid, "🎵 Битрейт"); Button.secondary("voice_bitrate:" + channel.getId(), "🎵 Битрейт"),
Button deleteBtn = Button.danger("voice_delete:" + cid, "🗑️ Удалить"); Button.danger("voice_delete:" + channel.getId(), "🗑️ Удалить"));
channel.sendMessageEmbeds(embed.build()).addComponents(row).queue();
channel
.sendMessageEmbeds(embed.build())
.addComponents(ActionRow.of(lockBtn, limitBtn, renameBtn, bitrateBtn, deleteBtn))
.queue();
} }
public void saveUserPreset(String userId, VoicePreset preset) { public void saveUserPreset(String userId, VoicePreset preset) {
@ -155,28 +127,19 @@ public class VoiceChannelManager {
} }
public VoicePreset getUserPreset(String userId) { public VoicePreset getUserPreset(String userId) {
VoicePreset preset = presetRepo.getPreset(userId); return presetRepo.getPreset(userId) != null ? presetRepo.getPreset(userId) : new VoicePreset();
return preset != null ? preset : new VoicePreset();
} }
public void applyUserPreset(VoiceChannel channel, String userId) { public void applyUserPreset(VoiceChannel channel, String userId) {
VoicePreset preset = getUserPreset(userId); var preset = getUserPreset(userId);
try { try {
if (preset.getName() != null && !preset.getName().isEmpty()) { if (preset.getName() != null && !preset.getName().isEmpty())
channel.getManager().setName(preset.getName()).queue(); channel.getManager().setName(preset.getName()).queue();
} if (preset.getLimit() >= 0) channel.getManager().setUserLimit(preset.getLimit()).queue();
if (preset.getLimit() >= 0) { if (preset.getBitrate() > 0) channel.getManager().setBitrate(preset.getBitrate()).queue();
channel.getManager().setUserLimit(preset.getLimit()).queue();
}
if (preset.getBitrate() > 0) {
channel.getManager().setBitrate(preset.getBitrate()).queue();
}
if (preset.isLocked()) { if (preset.isLocked()) {
channel var publicRole = channel.getGuild().getPublicRole();
.upsertPermissionOverride(channel.getGuild().getPublicRole()) channel.upsertPermissionOverride(publicRole).deny(Permission.VOICE_CONNECT).queue();
.deny(Permission.VOICE_CONNECT)
.queue();
} }
LoggerDecorator.voice( LoggerDecorator.voice(
logger, "Применён пресет пользователя " + userId + " к каналу " + channel.getId()); logger, "Применён пресет пользователя " + userId + " к каналу " + channel.getId());

View file

@ -9,10 +9,9 @@ import creator.loki.embed.EmbedFactory;
import creator.loki.feature.config.repository.ConfigRepository; import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.util.GuildChannelResolve; import creator.loki.util.GuildChannelResolve;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.channel.concrete.Category;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
public class VoiceSetupPrefixCommand implements Command { public class VoiceSetupPrefixCommand implements Command {
private final VoiceChannelManager voiceChannelManager; private final VoiceChannelManager voiceChannelManager;
private final ConfigRepository configRepo; private final ConfigRepository configRepo;
@ -48,13 +47,11 @@ public class VoiceSetupPrefixCommand implements Command {
context.getChannel().sendMessage("❌ У вас нет прав администратора!").queue(); context.getChannel().sendMessage("❌ У вас нет прав администратора!").queue();
return; return;
} }
var args = context.getArgs();
List<String> args = context.getArgs();
if (args.isEmpty()) { if (args.isEmpty()) {
sendHelp(context); sendHelp(context);
return; return;
} }
switch (args.get(0).toLowerCase()) { switch (args.get(0).toLowerCase()) {
case "trigger" -> handleTrigger(context, args); case "trigger" -> handleTrigger(context, args);
case "category" -> handleCategory(context, args); case "category" -> handleCategory(context, args);
@ -65,17 +62,12 @@ public class VoiceSetupPrefixCommand implements Command {
private void handleTrigger(CommandContext context, List<String> args) { private void handleTrigger(CommandContext context, List<String> args) {
if (args.size() < 2) { if (args.size() < 2) {
context var msg =
.getChannel() "❌ Укажите голосовой канал: `#канал`, `<#id>` или числовой id.\nПример: `!voice-setup trigger 123456789012345678`";
.sendMessage( context.getChannel().sendMessage(msg).queue();
"❌ Укажите голосовой канал: `#канал`, `<#id>` или числовой id."
+ "\nПример: `!voice-setup trigger 123456789012345678`")
.queue();
return; return;
} }
var channel = GuildChannelResolve.voice(context.getGuild(), context.getMessage(), args, 1);
VoiceChannel channel =
GuildChannelResolve.voice(context.getGuild(), context.getMessage(), args, 1);
if (channel == null) { if (channel == null) {
context context
.getChannel() .getChannel()
@ -85,34 +77,24 @@ public class VoiceSetupPrefixCommand implements Command {
return; return;
} }
configRepo.set("voice.trigger_channel_id", channel.getId()); configRepo.set("voice.trigger_channel_id", channel.getId());
var embed =
context EmbedFactory.create()
.getChannel() .withTitle("✅ Канал-триггер установлен")
.sendMessageEmbeds( .withDescription(
EmbedFactory.create() "Теперь при входе в " + channel.getAsMention() + " будет создаваться личный канал")
.withTitle("✅ Канал-триггер установлен") .withColor(Color.GREEN)
.withDescription( .build();
"Теперь при входе в " context.getChannel().sendMessageEmbeds(embed).queue();
+ channel.getAsMention()
+ " будет создаваться личный канал")
.withColor(Color.GREEN)
.build())
.queue();
} }
private void handleCategory(CommandContext context, List<String> args) { private void handleCategory(CommandContext context, List<String> args) {
if (args.size() < 2) { if (args.size() < 2) {
context var msg =
.getChannel() "❌ Укажите категорию: `#категория`, `<#id>` или числовой id.\nПример: `!voice-setup category 123456789012345678`";
.sendMessage( context.getChannel().sendMessage(msg).queue();
"❌ Укажите категорию: `#категория`, `<#id>` или числовой id."
+ "\nПример: `!voice-setup category 123456789012345678`")
.queue();
return; return;
} }
var category = GuildChannelResolve.category(context.getGuild(), context.getMessage(), args, 1);
Category category =
GuildChannelResolve.category(context.getGuild(), context.getMessage(), args, 1);
if (category == null) { if (category == null) {
context context
.getChannel() .getChannel()
@ -122,60 +104,41 @@ public class VoiceSetupPrefixCommand implements Command {
return; return;
} }
configRepo.set("voice.category_id", category.getId()); configRepo.set("voice.category_id", category.getId());
var embed =
context EmbedFactory.create()
.getChannel() .withTitle("✅ Категория установлена")
.sendMessageEmbeds( .withDescription(
EmbedFactory.create() "Личные каналы будут создаваться в категории: **" + category.getName() + "**")
.withTitle("✅ Категория установлена") .withColor(Color.GREEN)
.withDescription( .build();
"Личные каналы будут создаваться в категории: **" + category.getName() + "**") context.getChannel().sendMessageEmbeds(embed).queue();
.withColor(Color.GREEN)
.build())
.queue();
} }
private void handleInfo(CommandContext context) { private void handleInfo(CommandContext context) {
String triggerId = configRepo.get("voice.trigger_channel_id"); var triggerId = configRepo.get("voice.trigger_channel_id");
String categoryId = configRepo.get("voice.category_id"); var categoryId = configRepo.get("voice.category_id");
var embed = var embed =
EmbedFactory.create() EmbedFactory.create()
.withTitle("🎤 Конфигурация системы личных каналов") .withTitle("🎤 Конфигурация системы личных каналов")
.withColor(Color.BLUE); .withColor(Color.BLUE);
if (triggerId != null) embed.addField("Канал-триггер", "<#" + triggerId + ">", false);
if (triggerId != null) { else embed.addField("Канал-триггер", "❌ Не установлен", false);
embed.addField("Канал-триггер", "<#" + triggerId + ">", false);
} else {
embed.addField("Канал-триггер", "❌ Не установлен", false);
}
if (categoryId != null) { if (categoryId != null) {
Category category = context.getGuild().getCategoryById(categoryId); var category = context.getGuild().getCategoryById(categoryId);
embed.addField("Категория", category != null ? category.getName() : "❌ Не найдена", false); embed.addField("Категория", category != null ? category.getName() : "❌ Не найдена", false);
} else { } else embed.addField("Категория", "❌ Не установлена", false);
embed.addField("Категория", "❌ Не установлена", false);
}
context.getChannel().sendMessageEmbeds(embed.build()).queue(); context.getChannel().sendMessageEmbeds(embed.build()).queue();
} }
private void sendHelp(CommandContext context) { private void sendHelp(CommandContext context) {
var desc =
"**Доступные команды:**\n• `!voice-setup trigger` - `#войс` или числовой id канала\n• `!voice-setup category` - `#категория` или id\n• `!voice-setup info` - Показать текущую конфигурацию\n\nТекстовые каналы для управления создаются автоматически при создании войса.\nТолько для администраторов!";
context context
.getChannel() .getChannel()
.sendMessageEmbeds( .sendMessageEmbeds(
EmbedFactory.create() EmbedFactory.create()
.withTitle("🎤 Настройка голосовых каналов") .withTitle("🎤 Настройка голосовых каналов")
.withDescription( .withDescription(desc)
"""
**Доступные команды:**
• `!voice-setup trigger` - `#войс` или числовой id канала
• `!voice-setup category` - `#категория` или id
• `!voice-setup info` - Показать текущую конфигурацию
Текстовые каналы для управления создаются автоматически при создании войса.
Только для администраторов!
""")
.withColor(Color.BLUE) .withColor(Color.BLUE)
.build()) .build())
.queue(); .queue();

View file

@ -1,57 +1,69 @@
package creator.loki.integration.nekos; package creator.loki.integration.nekos;
import java.net.URI; import java.io.IOException;
import java.net.http.HttpClient; import java.util.concurrent.TimeUnit;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** Утилита для работы с nekos.life API */ @Singleton
public class NekosLifeApi { public class NekosLifeApi {
private static final Logger logger = LoggerFactory.getLogger(NekosLifeApi.class); private static final Logger logger = LoggerFactory.getLogger(NekosLifeApi.class);
private static final String API_BASE = "https://nekos.life/api/v2/img/"; private static final String API_BASE = "https://nekos.life/api/v2/img/";
private static final HttpClient CLIENT =
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
/** Получить URL изображения из nekos.life API с повторными попытками */ private static NekosLifeApi instance;
public static String fetchImage(String category) {
private final OkHttpClient client;
@Inject
public NekosLifeApi() {
this.client =
new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build();
NekosLifeApi.instance = this;
}
NekosLifeApi(OkHttpClient client) {
this.client = client;
NekosLifeApi.instance = this;
}
public String getImage(String category) {
int maxRetries = 3; int maxRetries = 3;
int retryDelay = 1000; long retryDelay = 1000;
for (int attempt = 1; attempt <= maxRetries; attempt++) { for (int attempt = 1; attempt <= maxRetries; attempt++) {
try { Request request = new Request.Builder().url(API_BASE + category).get().build();
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(API_BASE + category))
.timeout(Duration.ofSeconds(10))
.GET()
.build();
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
if (response.statusCode() == 200) { if (response.code() >= 500) {
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject(); logger.warn(
return json.get("url").getAsString(); "Nekos.life API error: {} (attempt {}/{})", response.code(), attempt, maxRetries);
} else if (response.statusCode() >= 500) { } else {
logger.warn( logger.error("Nekos.life API error: {}", response.code());
"Nekos.life API error: {} (attempt {}/{})", return null;
response.statusCode(), }
attempt,
maxRetries);
} else { } else {
logger.error("Nekos.life API error: {}", response.statusCode()); ResponseBody body = response.body();
return null; if (body == null) {
logger.warn("Empty response body from nekos.life (attempt {}/{})", attempt, maxRetries);
continue;
}
JsonObject json = JsonParser.parseString(body.string()).getAsJsonObject();
return json.get("url").getAsString();
} }
} catch (InterruptedException e) { } catch (IOException e) {
Thread.currentThread().interrupt();
logger.error("Interrupted while accessing nekos.life API", e);
return null;
} catch (Exception e) {
logger.error( logger.error(
"Error fetching image from nekos.life (attempt {}/{})", attempt, maxRetries, e); "Error fetching image from nekos.life (attempt {}/{})", attempt, maxRetries, e);
} }
@ -70,4 +82,11 @@ public class NekosLifeApi {
logger.error("Failed to fetch image after {} attempts", maxRetries); logger.error("Failed to fetch image after {} attempts", maxRetries);
return null; return null;
} }
public static String fetchImage(String category) {
if (instance == null) {
instance = new NekosLifeApi();
}
return instance.getImage(category);
}
} }

View file

@ -0,0 +1,39 @@
package creator.loki.util;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
public final class VoiceHelper {
private static final int RETRIES = 10;
private static final long DELAY_MS = 200;
private VoiceHelper() {}
public static boolean waitForCondition(BooleanSupplier condition) {
for (int i = 0; i < RETRIES; i++) {
if (condition.getAsBoolean()) return true;
try {
Thread.sleep(DELAY_MS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
public static boolean waitForCondition(BooleanSupplier condition, long timeout, TimeUnit unit) {
long deadline = System.nanoTime() + unit.toNanos(timeout);
long delay = Math.min(DELAY_MS, unit.toMillis(timeout) / RETRIES);
while (System.nanoTime() < deadline) {
if (condition.getAsBoolean()) return true;
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
}

View file

@ -15,6 +15,7 @@ import creator.loki.feature.counter.repository.CounterStatsRepository;
import creator.loki.feature.economy.EconomyCommandRegistrar; import creator.loki.feature.economy.EconomyCommandRegistrar;
import creator.loki.feature.economy.service.*; import creator.loki.feature.economy.service.*;
import creator.loki.feature.entertainment.EntertainmentCommandRegistrar; import creator.loki.feature.entertainment.EntertainmentCommandRegistrar;
import creator.loki.feature.entertainment.service.RatingService;
import creator.loki.feature.level.LevelCommandRegistrar; import creator.loki.feature.level.LevelCommandRegistrar;
import creator.loki.feature.level.LevelRoleService; import creator.loki.feature.level.LevelRoleService;
import creator.loki.feature.level.ResetLevelsCommand; import creator.loki.feature.level.ResetLevelsCommand;
@ -101,7 +102,10 @@ class SmokeCommandRegistrationTest {
var entertainmentRegistrar = var entertainmentRegistrar =
new EntertainmentCommandRegistrar( new EntertainmentCommandRegistrar(
mock(CooldownManager.class), mock(ShipPairRepository.class), mock(BotConfig.class)); mock(CooldownManager.class),
mock(RatingService.class),
mock(ShipPairRepository.class),
mock(BotConfig.class));
entertainmentRegistrar.register(registry); entertainmentRegistrar.register(registry);
var utilityRegistrar = var utilityRegistrar =

View file

@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.*;
import java.awt.Color; import java.awt.Color;
import java.util.List; import java.util.List;
import creator.loki.feature.entertainment.commands.rating.RatingConfig;
import creator.loki.feature.entertainment.commands.rating.RatingLevel;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;

View file

@ -9,16 +9,20 @@ import java.util.function.Consumer;
import creator.loki.command.CommandContext; import creator.loki.command.CommandContext;
import creator.loki.feature.moderation.commands.utils.SlowmodeCommand; import creator.loki.feature.moderation.commands.utils.SlowmodeCommand;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.SelfUser;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.managers.channel.concrete.TextChannelManager; import net.dv8tion.jda.api.managers.channel.concrete.TextChannelManager;
import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@ -35,7 +39,17 @@ class SlowmodeCommandTest {
@Mock Member member; @Mock Member member;
@Mock Member selfMember; @Mock Member selfMember;
@Mock Guild guild; @Mock Guild guild;
@Mock JDA jda;
@Mock MessageCreateAction sendEmbedAction; @Mock MessageCreateAction sendEmbedAction;
@BeforeEach
void setUp() {
lenient().when(guild.getJDA()).thenReturn(jda);
SelfUser selfUser = mock(SelfUser.class);
lenient().when(jda.getSelfUser()).thenReturn(selfUser);
lenient().when(guild.getMember(selfUser)).thenReturn(selfMember);
}
@Mock TextChannelManager setSlowmodeAction; @Mock TextChannelManager setSlowmodeAction;
@Mock TextChannelManager channelManager; @Mock TextChannelManager channelManager;
@ -95,7 +109,7 @@ class SlowmodeCommandTest {
lenient().when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendEmbedAction); lenient().when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendEmbedAction);
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);
when(guild.getSelfMember()).thenReturn(selfMember); when(guild.getMember(any(User.class))).thenReturn(selfMember);
when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(false); .thenReturn(false);
var context = new CommandContext(message, List.of(), channel, member, guild); var context = new CommandContext(message, List.of(), channel, member, guild);
@ -111,7 +125,7 @@ class SlowmodeCommandTest {
when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendEmbedAction); when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendEmbedAction);
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);
when(guild.getSelfMember()).thenReturn(selfMember); when(guild.getMember(any(User.class))).thenReturn(selfMember);
when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);
when(channel.getSlowmode()).thenReturn(10); when(channel.getSlowmode()).thenReturn(10);
@ -129,7 +143,7 @@ class SlowmodeCommandTest {
when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendEmbedAction); when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendEmbedAction);
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);
when(guild.getSelfMember()).thenReturn(selfMember); when(guild.getMember(any(User.class))).thenReturn(selfMember);
when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);
@ -147,7 +161,7 @@ class SlowmodeCommandTest {
when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction); when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction);
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);
when(guild.getSelfMember()).thenReturn(selfMember); when(guild.getMember(any(User.class))).thenReturn(selfMember);
when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);
when(channel.getAsMention()).thenReturn("#general"); when(channel.getAsMention()).thenReturn("#general");
@ -170,7 +184,7 @@ class SlowmodeCommandTest {
when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction); when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction);
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);
when(guild.getSelfMember()).thenReturn(selfMember); when(guild.getMember(any(User.class))).thenReturn(selfMember);
when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);
@ -187,7 +201,7 @@ class SlowmodeCommandTest {
when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction); when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction);
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);
when(guild.getSelfMember()).thenReturn(selfMember); when(guild.getMember(any(User.class))).thenReturn(selfMember);
when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);
@ -205,7 +219,7 @@ class SlowmodeCommandTest {
when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction); when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction);
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);
when(guild.getSelfMember()).thenReturn(selfMember); when(guild.getMember(any(User.class))).thenReturn(selfMember);
when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
.thenReturn(true); .thenReturn(true);

View file

@ -32,9 +32,9 @@ class RankCardGeneratorTest {
} }
@Test @Test
void backgroundNotFoundThrowsException() { void backgroundNotFoundReturnsFallback() throws Exception {
var data = mockProfile(1, 0, 100, "nonexistent.png"); var data = mockProfile(1, 0, 100, "nonexistent.png");
assertThrows(Exception.class, () -> new RankCardGenerator().generate(data)); assertNotNull(new RankCardGenerator().generate(data));
} }
private static ProfileData mockProfile(int level, int xp, int maxXp, String background) { private static ProfileData mockProfile(int level, int xp, int maxXp, String background) {