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:
parent
42948520e8
commit
0d916fbd96
79 changed files with 2639 additions and 3245 deletions
32
.classpath
Normal file
32
.classpath
Normal 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
34
.project
Normal 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>
|
||||
13
.settings/org.eclipse.buildship.core.prefs
Normal file
13
.settings/org.eclipse.buildship.core.prefs
Normal 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
145
TODO.md
|
|
@ -251,7 +251,150 @@ feature/economy/
|
|||
|
||||
---
|
||||
|
||||
**Версия:** 2.3 | **Последнее обновление:** 2026-07-02 | **Java 21 ✅, God Constructor ✅, Critical баги ✅, Economy ✅, Level Roles ✅, Lavalink Music ✅, SpotBugs ✅, Checkstyle ✅, CI/CD ✅**
|
||||
---
|
||||
|
||||
## 🔴 Результаты аудита KISS/DRY/SOLID (2026-07-04)
|
||||
|
||||
### DRY — дублирование кода (критично)
|
||||
|
||||
| # | Что дублируется | Где | Сколько раз | Статус |
|
||||
|---|----------------|-----|-------------|--------|
|
||||
| D1 | `getDeterministicValue()` — целиком | GayCommand, FatCommand, IqCommand, PpCommand, BoykisserCommand, MustacheCommand, RateCommand | 7 | ✅ RatingService |
|
||||
| D2 | Cooldown check boilerplate (5 строк) | Все entertainment команды | 10+ | ✅ RatingService |
|
||||
| D3 | `trunc()` / `err()` helpers | MuteCommand, WarnCommand, SwapAndCaseCommand, UnmuteCommand, PunishCommand | 5 | ✅ ModerationHelper |
|
||||
| D4 | Voice connect retry loop (цикл 10×200ms) | PlayCommand, RadioManager | 2 |
|
||||
| D5 | RatingConfig — 6 методов с идентичной структурой | RatingConfig.java | 6 |
|
||||
| D6 | Social команды (hug/kiss/slap/poke/tickle/cuddle/feed/pat) — шаблон | 10 файлов | 10 |
|
||||
|
||||
### SOLID
|
||||
|
||||
| # | Нарушение | Файл | Описание |
|
||||
|---|-----------|------|----------|
|
||||
| S1 | SRP | BotModule.java | Биндит всё приложение |
|
||||
| S2 | SRP | SwapAndCaseCommand.java | Две разные команды в одном файле |
|
||||
| S3 | SRP | BotInitializer.java | Инициализирует 3 JDA, Lavalink, mutes, counters, health |
|
||||
| S4 | SRP | ShutdownManager.java | 9 зависимостей в конструкторе |
|
||||
| S5 | SRP | PlayCommand.java | execute() на 145 строк |
|
||||
| S6 | OCP | Meter-команды (gay/fat/iq/pp) | Добавить новую = копировать 90 строк | ✅ RatingService |
|
||||
| S7 | DIP | MuteCommand, WarnCommand | >5 конкретных репозиториев вместо интерфейсов |
|
||||
|
||||
### Файлы >150 строк
|
||||
|
||||
| # | Файл | Строк |
|
||||
|---|------|-------|
|
||||
| F1 | PlayCommand.java | 219 |
|
||||
| F2 | QueueSlashHandler.java | 207 |
|
||||
| F3 | MusicBotControlHandler.java | 207 |
|
||||
| F4 | QueueCommands.java | 207 |
|
||||
| F5 | TrackScheduler.java | 202 |
|
||||
| F6 | MuteCommand.java | 199 |
|
||||
| F7 | ModerationLogger.java | 199 |
|
||||
| F8 | FeatureTables.java | 193 |
|
||||
| F9 | DatabaseMigration.java | 193 |
|
||||
| F10 | VoiceChannelManager.java | 187 |
|
||||
| F11 | ServerStatsService.java | 187 |
|
||||
| F12 | VoiceSetupPrefixCommand.java | 183 |
|
||||
| F13 | RadioManager.java | 182 |
|
||||
| F14 | MetricsCollector.java | 182 |
|
||||
| F15 | WarnCommand.java | 184 |
|
||||
| F16 | BotInitializer.java | 182 |
|
||||
| F17 | SlashHandler's (Queue/Filter) | >100 |
|
||||
| F18 | GenerateRatingsCommand.java | 172 |
|
||||
| F19 | BlackjackService.java | 172 |
|
||||
| F20 | SqlEconomyRepository.java | 169 |
|
||||
| F21 | GuildMusicManager.java | 167 |
|
||||
| F22 | ProfileCommand/RankCardGen | >150 |
|
||||
|
||||
### >3 файла в папке
|
||||
|
||||
| # | Папка | Файлов |
|
||||
|---|-------|--------|
|
||||
| P1 | feature/entertainment/commands/ | 15 |
|
||||
| P2 | feature/social/commands/ | 10 |
|
||||
| P3 | feature/economy/command/minigame/ | 7 |
|
||||
| P4 | feature/level/ (все субпапки) | 21 |
|
||||
| P5 | feature/moderation/ (все субпакеты) | 18 |
|
||||
| P6 | feature/music/ (все субпакеты) | 22 |
|
||||
| P7 | feature/voice/ (все субпакеты) | 12 |
|
||||
|
||||
### Нет тестов
|
||||
|
||||
| # | Фича | Файлов |
|
||||
|---|------|--------|
|
||||
| T1 | music | 20+ |
|
||||
| T2 | voice | 12 |
|
||||
| T3 | social | 10 |
|
||||
| T4 | role | 9 |
|
||||
| T5 | radio | 5 |
|
||||
| T6 | reputation | 4 |
|
||||
| T7 | counter | 7 |
|
||||
| T8 | admin | 1 |
|
||||
| T9 | help | 2 |
|
||||
| T10 | utility | 2 |
|
||||
| T11 | welcome | 2 |
|
||||
| T12 | profile | 5 |
|
||||
| T13 | serverstats | 1 |
|
||||
|
||||
## 📋 План рефакторинга (KISS/DRY/SOLID)
|
||||
|
||||
### Этап 8: Убить дублирование (DRY) — в процессе
|
||||
|
||||
- [x] D1/D2: `RatingService` с `getDeterministicValue()`, cooldown, member parsing — 7 команд переписаны
|
||||
- [x] D3: `ModerationHelper` с `trunc()`/`err()` — MuteCommand, UnmuteCommand, WarnCommand, SwapAndCaseCommand, PunishCommand
|
||||
- [ ] D4: Вынести voice connect retry в утилиту
|
||||
- [ ] D5: RatingConfig — заменить 6 методов на data-driven (Map или config)
|
||||
- [ ] D6: Social команды — единый `SocialCommand` с параметром action
|
||||
|
||||
### Этап 9: SOLID
|
||||
|
||||
- [ ] S1: Разбить `BotModule` на per-feature модули (FeatureModule → install в BotModule)
|
||||
- [ ] S2: Разделить `SwapAndCaseCommand` на два отдельных файла
|
||||
- [ ] S3: Выделить `MusicBotFactory`, `RadioBotFactory` из `BotInitializer`
|
||||
- [ ] S4: Уменьшить кол-во зависимостей `ShutdownManager` через события (EventBus)
|
||||
- [ ] S5: Разбить `PlayCommand.execute()` на методы
|
||||
- [ ] S6: Meter-команды сделать data-driven через `RatingService`
|
||||
- [ ] S7: Выделить интерфейсы для репозиториев/сервисов (DIP)
|
||||
|
||||
### Этап 10: Файлы ≤150 строк
|
||||
|
||||
- [ ] F01: PlayCommand.java — разбить на методы/классы
|
||||
- [ ] F02-04: QueueSlashHandler, MusicBotControlHandler, QueueCommands — вынести общие части
|
||||
- [ ] F05: TrackScheduler.java — вывести FilterState в отд. файл или утилиту
|
||||
- [ ] F06: MuteCommand.java — разбить на subcommand handler'ы
|
||||
- [ ] F07: ModerationLogger.java — разбить по типам событий
|
||||
- [ ] F08-09: FeatureTables, DatabaseMigration — разбить по фичам
|
||||
- [ ] F10-12: Voice* — вынести логику в сервисы
|
||||
- [ ] F13: RadioManager.java — вынести voice connect в утилиту
|
||||
- [ ] F14: MetricsCollector.java — разбить по типам метрик
|
||||
- [ ] F15: WarnCommand.java — вынести auto-mute в отдельный сервис
|
||||
- [ ] F16: BotInitializer.java — вынести фабрики ботов
|
||||
- [ ] Остальные: разбить по SOLID
|
||||
|
||||
### Этап 11: ≤6 файлов в папке
|
||||
|
||||
- [ ] P1: Entertainment команды — группировать по смыслу (coin/dice → `random/`, gay/iq/pp → `rating/`)
|
||||
- [ ] P2: Social команды — объединить в `SocialCommand` с параметром
|
||||
- [ ] P4: Level — разбить на commands/, service/, repository/
|
||||
- [ ] P5: Moderation — разбить по поддоменам (commands/punish, commands/utils, warn)
|
||||
- [ ] P6: Music — разбить на commands/, player/, lavalink/
|
||||
|
||||
### Этап 12: Тесты
|
||||
|
||||
- [ ] T1: Music — написать тесты на LavalinkManager, TrackScheduler, PlayCommand
|
||||
- [ ] T2: Voice — VoiceChannelManager, VoiceButtonHandler
|
||||
- [ ] T3: Social — интеграционные тесты с NekosLifeApi (мок)
|
||||
- [ ] T4: Radio — RadioManager, RadioCommand
|
||||
- [ ] T5: Role — CustomRoleManager, RoleCommandValidator
|
||||
- [ ] T6-13: Остальные фичи — минимум smoke test
|
||||
|
||||
### Техдолг (баги)
|
||||
|
||||
- [ ] B1: EmbedFactoryTest — 2 падающих теста (`withTitle`, `withColor`) — IllegalStateException
|
||||
- [ ] B2: SlowmodeCommandTest — починен (было 7 ошибок компиляции из-за JDA 6.4 SelfMember)
|
||||
|
||||
---
|
||||
|
||||
**Версия:** 2.3 | **Последнее обновление:** 2026-07-04 | **Java 21 ✅, God Constructor ✅, Critical баги ✅, Economy ✅, Level Roles ✅, Lavalink Music ✅, SpotBugs ✅, Checkstyle ✅, CI/CD ✅**
|
||||
|
||||
## ✅ Что добавлено в v2.3
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ dependencies {
|
|||
implementation libs.guice
|
||||
implementation libs.sqlite.jdbc
|
||||
implementation libs.hikaricp
|
||||
implementation libs.okhttp
|
||||
implementation libs.logging.interceptor
|
||||
|
||||
testImplementation libs.junit.jupiter
|
||||
testImplementation libs.mockito.core
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ caffeine = "3.2.0"
|
|||
guice = "7.0.0"
|
||||
sqlite = "3.49.1.0"
|
||||
hikaricp = "6.3.0"
|
||||
okhttp = "5.4.0"
|
||||
junit = "5.12.1"
|
||||
mockito = "5.15.2"
|
||||
junitPlatformLauncher = "1.12.1"
|
||||
|
|
@ -29,6 +30,8 @@ caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "c
|
|||
guice = { module = "com.google.inject:guice", version.ref = "guice" }
|
||||
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
|
||||
hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikaricp" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
|
||||
mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package creator.loki.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.core.registry.PrefixCommandRegistrar;
|
||||
|
|
@ -63,119 +65,92 @@ public class BotInitializer {
|
|||
public BotCluster start() {
|
||||
LoggerDecorator.startup(logger, "Инициализация main бота...");
|
||||
registrar.registerAll();
|
||||
JDA mainJda = createMainJDA();
|
||||
try {
|
||||
mainJda.awaitReady();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Main бот прерван во время ожидания готовности", e);
|
||||
}
|
||||
JDA mainJda = buildMainJDA();
|
||||
mainJda.updateCommands().queue();
|
||||
muteScheduler.restorePending(mainJda);
|
||||
for (var guild : mainJda.getGuilds()) {
|
||||
memberCounterListener.startScheduler(guild);
|
||||
}
|
||||
mainJda.getGuilds().forEach(g -> memberCounterListener.startScheduler(g));
|
||||
healthHttpServer.start();
|
||||
|
||||
JDA musicJda = null;
|
||||
if (config.getMusicToken() != null && !config.getMusicToken().isBlank()) {
|
||||
LoggerDecorator.startup(logger, "Инициализация music бота...");
|
||||
musicJda = createMusicJDA();
|
||||
try {
|
||||
musicJda.awaitReady();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Music бот прерван во время ожидания готовности", e);
|
||||
}
|
||||
}
|
||||
|
||||
JDA radioJda = null;
|
||||
if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) {
|
||||
LoggerDecorator.startup(logger, "Инициализация radio бота...");
|
||||
radioJda = createRadioJDA();
|
||||
try {
|
||||
radioJda.awaitReady();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Radio бот прерван во время ожидания готовности", e);
|
||||
}
|
||||
JDA musicJda = createMusicJDA();
|
||||
JDA radioJda = createRadioJDA();
|
||||
if (radioJda != null) {
|
||||
radioManager.setRadioJda(radioJda);
|
||||
radioManager.setLavalinkManager(lavalinkManager);
|
||||
}
|
||||
|
||||
BotCluster cluster = new BotCluster(mainJda, musicJda, radioJda);
|
||||
registerShutdownHook(cluster);
|
||||
Runtime.getRuntime()
|
||||
.addShutdownHook(new Thread(() -> shutdownManager.shutdown(cluster), "ShutdownHook"));
|
||||
return cluster;
|
||||
}
|
||||
|
||||
private JDA createMainJDA() {
|
||||
private JDA buildMainJDA() {
|
||||
var builder =
|
||||
JDABuilder.createDefault(config.getMainToken())
|
||||
.setStatus(OnlineStatus.ONLINE)
|
||||
.setActivity(Activity.playing(config.getActivity()))
|
||||
.enableIntents(
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_MEMBERS,
|
||||
GatewayIntent.GUILD_VOICE_STATES)
|
||||
.enableCache(CacheFlag.VOICE_STATE)
|
||||
.addEventListeners(listenerAggregator.getAll());
|
||||
|
||||
createBuilder(
|
||||
config.getMainToken(),
|
||||
config.getActivity(),
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_MEMBERS,
|
||||
GatewayIntent.GUILD_VOICE_STATES);
|
||||
if (lavalinkManager.getClient() != null) {
|
||||
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient()));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
builder.addEventListeners(listenerAggregator.getAll());
|
||||
return awaitJda(builder.build());
|
||||
}
|
||||
|
||||
private JDA createMusicJDA() {
|
||||
LavalinkManager musicLavalink =
|
||||
new LavalinkManager(config, queueRepository, config.getMusicToken());
|
||||
|
||||
if (config.getMusicToken() == null || config.getMusicToken().isBlank()) return null;
|
||||
LoggerDecorator.startup(logger, "Инициализация music бота...");
|
||||
LavalinkManager ml = new LavalinkManager(config, queueRepository, config.getMusicToken());
|
||||
var builder =
|
||||
JDABuilder.createDefault(config.getMusicToken())
|
||||
.setStatus(OnlineStatus.ONLINE)
|
||||
.setActivity(Activity.playing("🎵 q!play"))
|
||||
.enableIntents(
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_VOICE_STATES)
|
||||
.enableCache(CacheFlag.VOICE_STATE);
|
||||
|
||||
if (musicLavalink.getClient() != null) {
|
||||
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(musicLavalink.getClient()));
|
||||
createBuilder(
|
||||
config.getMusicToken(),
|
||||
"🎵 q!play",
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_VOICE_STATES);
|
||||
if (ml.getClient() != null) {
|
||||
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(ml.getClient()));
|
||||
}
|
||||
|
||||
builder.addEventListeners(
|
||||
new MusicBotListener(
|
||||
musicLavalink,
|
||||
queueRepository,
|
||||
new creator.loki.feature.music.core.PlayerManager(musicLavalink)));
|
||||
|
||||
return builder.build();
|
||||
ml, queueRepository, new creator.loki.feature.music.core.PlayerManager(ml)));
|
||||
return awaitJda(builder.build());
|
||||
}
|
||||
|
||||
private JDA createRadioJDA() {
|
||||
if (config.getRadioToken() == null || config.getRadioToken().isBlank()) return null;
|
||||
LoggerDecorator.startup(logger, "Инициализация radio бота...");
|
||||
var builder =
|
||||
JDABuilder.createDefault(config.getRadioToken())
|
||||
.setStatus(OnlineStatus.ONLINE)
|
||||
.setActivity(Activity.playing("📻 r!play"))
|
||||
.enableIntents(
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_VOICE_STATES)
|
||||
.enableCache(CacheFlag.VOICE_STATE);
|
||||
|
||||
createBuilder(
|
||||
config.getRadioToken(),
|
||||
"📻 r!play",
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_VOICE_STATES);
|
||||
if (lavalinkManager.getClient() != null) {
|
||||
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient()));
|
||||
}
|
||||
|
||||
builder.addEventListeners(new RadioBotListener(radioManager));
|
||||
|
||||
return builder.build();
|
||||
return awaitJda(builder.build());
|
||||
}
|
||||
|
||||
private void registerShutdownHook(BotCluster cluster) {
|
||||
Runtime.getRuntime()
|
||||
.addShutdownHook(new Thread(() -> shutdownManager.shutdown(cluster), "ShutdownHook"));
|
||||
private static JDABuilder createBuilder(String token, String activity, GatewayIntent... intents) {
|
||||
return JDABuilder.createDefault(token)
|
||||
.setStatus(OnlineStatus.ONLINE)
|
||||
.setActivity(Activity.playing(activity))
|
||||
.enableIntents(List.of(intents))
|
||||
.enableCache(CacheFlag.VOICE_STATE);
|
||||
}
|
||||
|
||||
private static JDA awaitJda(JDA jda) {
|
||||
try {
|
||||
jda.awaitReady();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("JDA прерван во время ожидания готовности", e);
|
||||
}
|
||||
return jda;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,10 @@ import creator.loki.config.BotConfig;
|
|||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.rating.repository.RatingsRepository;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Команда для генерации всех rating значений для всех участников сервера Доступна только
|
||||
* супер-админу
|
||||
*/
|
||||
public class GenerateRatingsCommand implements Command {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GenerateRatingsCommand.class);
|
||||
private final RatingsRepository ratingsRepository;
|
||||
|
|
@ -53,120 +50,98 @@ public class GenerateRatingsCommand implements Command {
|
|||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("❌ Недостаточно прав")
|
||||
.withDescription("Эта команда доступна только супер-администратору бота.")
|
||||
.withColor(Color.RED)
|
||||
.build())
|
||||
errorEmbed(
|
||||
"❌ Недостаточно прав", "Эта команда доступна только супер-администратору бота."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.getGuild() == null) {
|
||||
context.getChannel().sendMessage("❌ Команда работает только на сервере.").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
var initial =
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("⏳ Генерация рейтингов...")
|
||||
.withDescription(
|
||||
"Начинаю генерацию всех rating значений для участников сервера...")
|
||||
.withColor(Color.YELLOW)
|
||||
.build())
|
||||
warnEmbed(
|
||||
"⏳ Генерация рейтингов...",
|
||||
"Начинаю генерацию всех rating значений для участников сервера..."))
|
||||
.complete();
|
||||
|
||||
try {
|
||||
String guildId = context.getGuild().getId();
|
||||
int memberCount = 0;
|
||||
int generatedCount = 0;
|
||||
|
||||
var guildId = context.getGuild().getId();
|
||||
var memberCount = 0;
|
||||
var generatedCount = 0;
|
||||
for (Member member : context.getGuild().getMembers()) {
|
||||
if (member.getUser().isBot()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (member.getUser().isBot()) continue;
|
||||
memberCount++;
|
||||
String userId = member.getUser().getId();
|
||||
|
||||
generateRatingForUser(guildId, userId, "ship");
|
||||
generateRatingForUser(guildId, userId, "gay");
|
||||
generateRatingForUser(guildId, userId, "iq");
|
||||
generateRatingForUser(guildId, userId, "pp");
|
||||
generateRatingForUser(guildId, userId, "mustache");
|
||||
generateRatingForUser(guildId, userId, "fat");
|
||||
generateRatingForUser(guildId, userId, "boykisser");
|
||||
|
||||
var userId = member.getUser().getId();
|
||||
for (var type : new String[] {"ship", "gay", "iq", "pp", "mustache", "fat", "boykisser"}) {
|
||||
generateRatingForUser(guildId, userId, type);
|
||||
}
|
||||
generatedCount++;
|
||||
}
|
||||
|
||||
initial
|
||||
.editMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Генерация завершена")
|
||||
.withDescription(
|
||||
"Успешно сгенерированы все rating значения!\n\n"
|
||||
+ "**Статистика:**\n"
|
||||
+ "• Обработано участников: **"
|
||||
+ memberCount
|
||||
+ "**\n"
|
||||
+ "• Сгенерировано значений: **"
|
||||
+ (generatedCount * 7)
|
||||
+ "**\n"
|
||||
+ "• Типы рейтингов: ship, gay, iq, pp, mustache, fat, boykisser")
|
||||
.withColor(Color.GREEN)
|
||||
.withFooter(
|
||||
"Инициатор: " + context.getAuthor().getUser().getName(),
|
||||
context.getAuthor().getAvatarUrl())
|
||||
.build())
|
||||
.queue();
|
||||
|
||||
initial.editMessageEmbeds(successEmbed(context, memberCount, generatedCount)).queue();
|
||||
logger.info(
|
||||
"Супер-админ {} сгенерировал рейтинги для {} участников",
|
||||
context.getAuthor().getUser().getName(),
|
||||
memberCount);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при генерации рейтингов", e);
|
||||
initial
|
||||
.editMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("❌ Ошибка")
|
||||
.withDescription("Произошла ошибка при генерации рейтингов: " + e.getMessage())
|
||||
.withColor(Color.RED)
|
||||
.build())
|
||||
errorEmbed("❌ Ошибка", "Произошла ошибка при генерации рейтингов: " + e.getMessage()))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
||||
/** Генерирует rating значение для пользователя */
|
||||
private void generateRatingForUser(String guildId, String userId, String ratingType) {
|
||||
// Проверяем существует ли уже значение
|
||||
if (ratingsRepository.hasRating(guildId, userId, ratingType)) {
|
||||
return; // Не перезаписываем существующие значения
|
||||
}
|
||||
|
||||
// Генерируем значение в зависимости от типа
|
||||
int value =
|
||||
if (ratingsRepository.hasRating(guildId, userId, ratingType)) return;
|
||||
var value =
|
||||
switch (ratingType) {
|
||||
case "iq" -> random.nextInt(201); // 0-200
|
||||
case "pp" -> random.nextInt(31); // 0-30 см
|
||||
case "iq" -> random.nextInt(201);
|
||||
case "pp" -> random.nextInt(31);
|
||||
case "mustache" -> {
|
||||
// Редкий шанс на легендарные усы
|
||||
if (random.nextInt(100) < 1) {
|
||||
yield 1000 + random.nextInt(500); // 1000-1500
|
||||
}
|
||||
yield random.nextInt(101); // 0-100
|
||||
if (random.nextInt(100) < 1) yield 1000 + random.nextInt(500);
|
||||
yield random.nextInt(101);
|
||||
}
|
||||
case "fat" -> random.nextInt(101); // 0-100%
|
||||
case "boykisser" -> random.nextInt(101); // 0-100%
|
||||
default -> random.nextInt(101); // ship, gay: 0-100%
|
||||
default -> random.nextInt(101);
|
||||
};
|
||||
|
||||
// Сохраняем значение в БД
|
||||
ratingsRepository.setRating(guildId, userId, ratingType, value);
|
||||
}
|
||||
|
||||
private MessageEmbed errorEmbed(String title, String description) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle(title)
|
||||
.withDescription(description)
|
||||
.withColor(Color.RED)
|
||||
.build();
|
||||
}
|
||||
|
||||
private MessageEmbed warnEmbed(String title, String description) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle(title)
|
||||
.withDescription(description)
|
||||
.withColor(Color.YELLOW)
|
||||
.build();
|
||||
}
|
||||
|
||||
private MessageEmbed successEmbed(CommandContext context, int memberCount, int generatedCount) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("✅ Генерация завершена")
|
||||
.withDescription(
|
||||
"Успешно сгенерированы все rating значения!\n\n"
|
||||
+ "**Статистика:**\n"
|
||||
+ "• Обработано участников: **"
|
||||
+ memberCount
|
||||
+ "**\n"
|
||||
+ "• Сгенерировано значений: **"
|
||||
+ (generatedCount * 7)
|
||||
+ "**\n"
|
||||
+ "• Типы рейтингов: ship, gay, iq, pp, mustache, fat, boykisser")
|
||||
.withColor(Color.GREEN)
|
||||
.withFooter(
|
||||
"Инициатор: " + context.getAuthor().getUser().getName(),
|
||||
context.getAuthor().getAvatarUrl())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package creator.loki.feature.economy.repository;
|
|||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
|
@ -19,24 +20,44 @@ public class SqlEconomyRepository implements EconomyRepository {
|
|||
private static final int MAX_TOP = 100;
|
||||
private final Database database;
|
||||
|
||||
private static final String COLUMNS =
|
||||
"user_id, cash, bank, total_earned, total_spent, last_daily, daily_streak, fishing_luck, efficiency, last_fish, job, last_work";
|
||||
|
||||
public SqlEconomyRepository(Database database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
private static String selectSql() {
|
||||
return "SELECT " + COLUMNS + " FROM economy WHERE user_id = ?";
|
||||
}
|
||||
|
||||
private static String upsertSql() {
|
||||
return "INSERT INTO economy ("
|
||||
+ COLUMNS
|
||||
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
+ "ON CONFLICT(user_id) DO UPDATE SET "
|
||||
+ "cash = excluded.cash, bank = excluded.bank, total_earned = excluded.total_earned, "
|
||||
+ "total_spent = excluded.total_spent, last_daily = excluded.last_daily, "
|
||||
+ "daily_streak = excluded.daily_streak, fishing_luck = excluded.fishing_luck, "
|
||||
+ "efficiency = excluded.efficiency, last_fish = excluded.last_fish, "
|
||||
+ "job = excluded.job, last_work = excluded.last_work";
|
||||
}
|
||||
|
||||
private static void setNullableString(PreparedStatement stmt, int index, String value)
|
||||
throws SQLException {
|
||||
if (value != null) {
|
||||
stmt.setString(index, value);
|
||||
} else {
|
||||
stmt.setNull(index, Types.VARCHAR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<EconomyAccount> findById(String userId) {
|
||||
String sql =
|
||||
"""
|
||||
SELECT user_id, cash, bank, total_earned, total_spent,
|
||||
last_daily, daily_streak, fishing_luck, efficiency,
|
||||
last_fish, job, last_work
|
||||
FROM economy
|
||||
WHERE user_id = ?
|
||||
""";
|
||||
|
||||
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||
var sql = selectSql();
|
||||
try (var stmt = database.prepare(sql)) {
|
||||
stmt.setString(1, userId);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return Optional.of(mapResultSetToAccount(rs));
|
||||
}
|
||||
|
|
@ -49,56 +70,20 @@ public class SqlEconomyRepository implements EconomyRepository {
|
|||
|
||||
@Override
|
||||
public void save(EconomyAccount account) {
|
||||
String sql =
|
||||
"""
|
||||
INSERT INTO economy (user_id, cash, bank, total_earned, total_spent,
|
||||
last_daily, daily_streak, fishing_luck, efficiency,
|
||||
last_fish, job, last_work)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id)
|
||||
DO UPDATE SET
|
||||
cash = excluded.cash,
|
||||
bank = excluded.bank,
|
||||
total_earned = excluded.total_earned,
|
||||
total_spent = excluded.total_spent,
|
||||
last_daily = excluded.last_daily,
|
||||
daily_streak = excluded.daily_streak,
|
||||
fishing_luck = excluded.fishing_luck,
|
||||
efficiency = excluded.efficiency,
|
||||
last_fish = excluded.last_fish,
|
||||
job = excluded.job,
|
||||
last_work = excluded.last_work
|
||||
""";
|
||||
|
||||
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||
var sql = upsertSql();
|
||||
try (var stmt = database.prepare(sql)) {
|
||||
stmt.setString(1, account.getUserId());
|
||||
stmt.setLong(2, account.getCash());
|
||||
stmt.setLong(3, account.getBank());
|
||||
stmt.setLong(4, account.getTotalEarned());
|
||||
stmt.setLong(5, account.getTotalSpent());
|
||||
if (account.getLastDaily() != null) {
|
||||
stmt.setString(6, account.getLastDaily());
|
||||
} else {
|
||||
stmt.setNull(6, java.sql.Types.VARCHAR);
|
||||
}
|
||||
setNullableString(stmt, 6, account.getLastDaily());
|
||||
stmt.setInt(7, account.getDailyStreak());
|
||||
stmt.setInt(8, account.getFishingLuck());
|
||||
stmt.setInt(9, account.getEfficiency());
|
||||
if (account.getLastFish() != null) {
|
||||
stmt.setString(10, account.getLastFish());
|
||||
} else {
|
||||
stmt.setNull(10, java.sql.Types.VARCHAR);
|
||||
}
|
||||
if (account.getJob() != null) {
|
||||
stmt.setString(11, account.getJob());
|
||||
} else {
|
||||
stmt.setNull(11, java.sql.Types.VARCHAR);
|
||||
}
|
||||
if (account.getLastWork() != null) {
|
||||
stmt.setString(12, account.getLastWork());
|
||||
} else {
|
||||
stmt.setNull(12, java.sql.Types.VARCHAR);
|
||||
}
|
||||
setNullableString(stmt, 10, account.getLastFish());
|
||||
setNullableString(stmt, 11, account.getJob());
|
||||
setNullableString(stmt, 12, account.getLastWork());
|
||||
stmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
logger.error("Error saving economy account for {}", account.getUserId(), e);
|
||||
|
|
@ -107,10 +92,10 @@ public class SqlEconomyRepository implements EconomyRepository {
|
|||
|
||||
@Override
|
||||
public boolean exists(String userId) {
|
||||
String sql = "SELECT 1 FROM economy WHERE user_id = ?";
|
||||
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||
var sql = "SELECT 1 FROM economy WHERE user_id = ?";
|
||||
try (var stmt = database.prepare(sql)) {
|
||||
stmt.setString(1, userId);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
return rs.next();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
|
|
@ -121,20 +106,15 @@ public class SqlEconomyRepository implements EconomyRepository {
|
|||
|
||||
@Override
|
||||
public List<EconomyAccount> findTopByBalance(int limit) {
|
||||
int lim = clampLimit(limit);
|
||||
String sql =
|
||||
"""
|
||||
SELECT user_id, cash, bank, total_earned, total_spent,
|
||||
last_daily, daily_streak, fishing_luck, efficiency,
|
||||
last_fish, job, last_work
|
||||
FROM economy
|
||||
ORDER BY (cash + bank) DESC, cash DESC, user_id ASC
|
||||
LIMIT ?
|
||||
""";
|
||||
List<EconomyAccount> list = new ArrayList<>();
|
||||
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||
var lim = clampLimit(limit);
|
||||
var sql =
|
||||
"SELECT "
|
||||
+ COLUMNS
|
||||
+ " FROM economy ORDER BY (cash + bank) DESC, cash DESC, user_id ASC LIMIT ?";
|
||||
var list = new ArrayList<EconomyAccount>();
|
||||
try (var stmt = database.prepare(sql)) {
|
||||
stmt.setInt(1, lim);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
list.add(mapResultSetToAccount(rs));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,41 +5,41 @@ import creator.loki.config.BotConfig;
|
|||
import creator.loki.core.registry.CommandRegistrar;
|
||||
import creator.loki.core.registry.CommandRegistry;
|
||||
import creator.loki.core.security.CooldownManager;
|
||||
import creator.loki.feature.entertainment.commands.BoykisserCommand;
|
||||
import creator.loki.feature.entertainment.commands.CoinCommand;
|
||||
import creator.loki.feature.entertainment.commands.DiceCommand;
|
||||
import creator.loki.feature.entertainment.commands.EightBallCommand;
|
||||
import creator.loki.feature.entertainment.commands.FatCommand;
|
||||
import creator.loki.feature.entertainment.commands.GayCommand;
|
||||
import creator.loki.feature.entertainment.commands.GlazbobaCommand;
|
||||
import creator.loki.feature.entertainment.commands.IqCommand;
|
||||
import creator.loki.feature.entertainment.commands.MemeCommand;
|
||||
import creator.loki.feature.entertainment.commands.MustacheCommand;
|
||||
import creator.loki.feature.entertainment.commands.PpCommand;
|
||||
import creator.loki.feature.entertainment.commands.RandomChoiceCommand;
|
||||
import creator.loki.feature.entertainment.commands.RateCommand;
|
||||
import creator.loki.feature.entertainment.commands.rating.BoykisserCommand;
|
||||
import creator.loki.feature.entertainment.commands.rating.FatCommand;
|
||||
import creator.loki.feature.entertainment.commands.rating.GayCommand;
|
||||
import creator.loki.feature.entertainment.commands.rating.IqCommand;
|
||||
import creator.loki.feature.entertainment.commands.rating.MustacheCommand;
|
||||
import creator.loki.feature.entertainment.commands.rating.PpCommand;
|
||||
import creator.loki.feature.entertainment.commands.rating.RateCommand;
|
||||
import creator.loki.feature.entertainment.service.RatingService;
|
||||
import creator.loki.feature.ship.ShipCommand;
|
||||
import creator.loki.feature.ship.repository.ShipPairRepository;
|
||||
import creator.loki.feature.social.commands.ComplimentCommand;
|
||||
import creator.loki.feature.social.commands.CuddleCommand;
|
||||
import creator.loki.feature.social.commands.FeedCommand;
|
||||
import creator.loki.feature.social.commands.RoastCommand;
|
||||
import creator.loki.feature.social.commands.HugCommand;
|
||||
import creator.loki.feature.social.commands.KissCommand;
|
||||
import creator.loki.feature.social.commands.PatCommand;
|
||||
import creator.loki.feature.social.commands.PokeCommand;
|
||||
import creator.loki.feature.social.commands.SlapCommand;
|
||||
import creator.loki.feature.social.commands.TickleCommand;
|
||||
import creator.loki.feature.social.commands.RoastCommand;
|
||||
import creator.loki.feature.social.commands.SocialCommand;
|
||||
|
||||
public class EntertainmentCommandRegistrar implements CommandRegistrar {
|
||||
private final CooldownManager cooldownManager;
|
||||
private final RatingService ratingService;
|
||||
private final ShipPairRepository shipPairRepository;
|
||||
private final BotConfig config;
|
||||
|
||||
@Inject
|
||||
public EntertainmentCommandRegistrar(
|
||||
CooldownManager cooldownManager, ShipPairRepository shipPairRepository, BotConfig config) {
|
||||
CooldownManager cooldownManager,
|
||||
RatingService ratingService,
|
||||
ShipPairRepository shipPairRepository,
|
||||
BotConfig config) {
|
||||
this.cooldownManager = cooldownManager;
|
||||
this.ratingService = ratingService;
|
||||
this.shipPairRepository = shipPairRepository;
|
||||
this.config = config;
|
||||
}
|
||||
|
|
@ -55,22 +55,18 @@ public class EntertainmentCommandRegistrar implements CommandRegistrar {
|
|||
registry.register(new ComplimentCommand());
|
||||
registry.register(new GlazbobaCommand());
|
||||
|
||||
registry.register(new IqCommand(cooldownManager));
|
||||
registry.register(new GayCommand(cooldownManager));
|
||||
registry.register(new PpCommand(cooldownManager));
|
||||
registry.register(new RateCommand(cooldownManager));
|
||||
registry.register(new BoykisserCommand(cooldownManager));
|
||||
registry.register(new MustacheCommand(cooldownManager));
|
||||
registry.register(new FatCommand(cooldownManager));
|
||||
registry.register(new IqCommand(ratingService));
|
||||
registry.register(new GayCommand(ratingService));
|
||||
registry.register(new PpCommand(ratingService));
|
||||
registry.register(new RateCommand(ratingService));
|
||||
registry.register(new BoykisserCommand(ratingService));
|
||||
registry.register(new MustacheCommand(ratingService));
|
||||
registry.register(new FatCommand(ratingService));
|
||||
|
||||
registry.register(new PatCommand(cooldownManager));
|
||||
registry.register(new HugCommand());
|
||||
registry.register(new KissCommand(cooldownManager));
|
||||
registry.register(new SlapCommand(cooldownManager));
|
||||
registry.register(new TickleCommand(cooldownManager));
|
||||
registry.register(new PokeCommand(cooldownManager));
|
||||
registry.register(new CuddleCommand(cooldownManager));
|
||||
registry.register(new FeedCommand(cooldownManager));
|
||||
for (var flavor : SocialCommand.Flavor.values()) {
|
||||
registry.register(new SocialCommand(flavor, cooldownManager));
|
||||
}
|
||||
|
||||
registry.register(
|
||||
new RandomChoiceCommand(config, "choose", RandomChoiceCommand.Flavor.DEFAULT));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,23 @@
|
|||
package creator.loki.feature.entertainment.commands;
|
||||
package creator.loki.feature.entertainment.commands.rating;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.core.security.CooldownManager;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.entertainment.service.RatingService;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
||||
/** Оценка пользователя из 10 с детерминированным результатом */
|
||||
public class RateCommand implements Command {
|
||||
|
||||
private final CooldownManager cooldownManager;
|
||||
private final RatingService ratingService;
|
||||
|
||||
public RateCommand(CooldownManager cooldownManager) {
|
||||
this.cooldownManager = cooldownManager;
|
||||
@Inject
|
||||
public RateCommand(RatingService ratingService) {
|
||||
this.ratingService = ratingService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -43,28 +42,13 @@ public class RateCommand implements Command {
|
|||
|
||||
@Override
|
||||
public void execute(CommandContext context) {
|
||||
// Проверка cooldown (5 секунд)
|
||||
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "rate")) {
|
||||
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "rate");
|
||||
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Member> mentioned = context.getMessage().getMentions().getMembers();
|
||||
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
|
||||
|
||||
if (!ratingService.checkAndApplyCooldown(context, "rate")) return;
|
||||
Member target = ratingService.resolveTarget(context);
|
||||
if (target == null) return;
|
||||
|
||||
// Применяем cooldown
|
||||
cooldownManager.applyCooldown(context.getAuthor().getId(), "rate", 5);
|
||||
|
||||
// Детерминированная оценка от 1 до 10
|
||||
int rating = getDeterministicValue(target.getId(), 420, 1, 10);
|
||||
|
||||
// Визуализация звездами
|
||||
int rating = ratingService.getDeterministicValue(target.getId(), 420, 1, 10);
|
||||
String stars = "⭐".repeat(rating) + "☆".repeat(10 - rating);
|
||||
|
||||
// Комментарий
|
||||
Map<Integer, String> comments = new HashMap<>();
|
||||
comments.put(10, "💯 ИДЕАЛЬНО!");
|
||||
comments.put(9, "🔥 Почти идеально!");
|
||||
|
|
@ -92,12 +76,4 @@ public class RateCommand implements Command {
|
|||
.build())
|
||||
.queue();
|
||||
}
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private int getDeterministicValue(String userId, int salt, int min, int max) {
|
||||
long seed = Long.parseLong(userId) + salt;
|
||||
RANDOM.setSeed(seed);
|
||||
return RANDOM.nextInt(max - min + 1) + min;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package creator.loki.feature.entertainment.commands;
|
||||
package creator.loki.feature.entertainment.commands.rating;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -13,10 +13,10 @@ import creator.loki.core.registry.CommandRegistry;
|
|||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.HelpCategories;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||
import net.dv8tion.jda.api.components.selections.StringSelectMenu;
|
||||
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
|
||||
@Singleton
|
||||
public class HelpMenuHandler extends ListenerAdapter {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import creator.loki.core.registry.CommandRegistry;
|
|||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.HelpCategories;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||
import net.dv8tion.jda.api.components.selections.StringSelectMenu;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
|
||||
public class HelpCommand implements Command {
|
||||
private final CommandRegistry registry;
|
||||
|
|
@ -75,7 +75,11 @@ public class HelpCommand implements Command {
|
|||
net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode(emoji));
|
||||
});
|
||||
|
||||
context.getChannel().sendMessageEmbeds(embed.build()).addComponents(ActionRow.of(menuBuilder.build())).queue();
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(embed.build())
|
||||
.addComponents(ActionRow.of(menuBuilder.build()))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private EmbedFactory createMainPage(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import java.util.concurrent.ThreadLocalRandom;
|
|||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import creator.loki.core.EventDispatcher;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.economy.service.EconomyService;
|
||||
import creator.loki.feature.level.domain.LevelUpResult;
|
||||
import creator.loki.feature.level.service.LevelService;
|
||||
|
|
@ -20,13 +19,16 @@ public class MessageListener extends ListenerAdapter {
|
|||
private final EventDispatcher dispatcher;
|
||||
private final LevelService levelService;
|
||||
private final EconomyService economyService;
|
||||
private final LevelUpNotifier levelUpNotifier;
|
||||
|
||||
@Inject
|
||||
public MessageListener(
|
||||
EventDispatcher dispatcher, LevelService levelService, EconomyService economyService) {
|
||||
EventDispatcher dispatcher, LevelService levelService,
|
||||
EconomyService economyService, LevelUpNotifier levelUpNotifier) {
|
||||
this.dispatcher = dispatcher;
|
||||
this.levelService = levelService;
|
||||
this.economyService = economyService;
|
||||
this.levelUpNotifier = levelUpNotifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -35,19 +37,10 @@ public class MessageListener extends ListenerAdapter {
|
|||
if (!event.getAuthor().isBot()) {
|
||||
String userId = event.getAuthor().getId();
|
||||
LevelUpResult levelUp = levelService.addMessageXP(userId);
|
||||
if (levelUp != null) {
|
||||
event
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success(
|
||||
"🎉 Повышение уровня!",
|
||||
event.getAuthor().getAsMention()
|
||||
+ " достиг **"
|
||||
+ levelUp.getNewLevel()
|
||||
+ " уровня**! Всего XP: **"
|
||||
+ levelUp.getTotalXP()
|
||||
+ "**"))
|
||||
.queue();
|
||||
if (levelUp != null && event.getMember() != null) {
|
||||
levelUpNotifier.sendLevelUpNotification(
|
||||
event.getMember(), levelUp.getOldLevel(), levelUp.getNewLevel(),
|
||||
event.getChannel());
|
||||
}
|
||||
int lcAmount = ThreadLocalRandom.current().nextInt(1, 4);
|
||||
economyService.earnCash(userId, lcAmount);
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** Обработчик кнопок подтверждения сброса уровней */
|
||||
@Singleton
|
||||
public class ResetLevelsButtonHandler extends ListenerAdapter {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ResetLevelsButtonHandler.class);
|
||||
private static final String PREFIX_CONFIRM = "reset_levels_confirm:";
|
||||
private static final String PREFIX_CANCEL = "reset_levels_cancel:";
|
||||
|
||||
private final ResetLevelsCommand resetCommand;
|
||||
|
||||
@Inject
|
||||
|
|
@ -26,87 +28,47 @@ public class ResetLevelsButtonHandler extends ListenerAdapter {
|
|||
public void onButtonInteraction(ButtonInteractionEvent event) {
|
||||
String buttonId = event.getComponentId();
|
||||
|
||||
// Проверяем что это наша кнопка
|
||||
if (!buttonId.startsWith("reset_levels_")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Извлекаем ID пользователя из кнопки
|
||||
String[] parts = buttonId.split(":");
|
||||
if (parts.length != 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
String authorId = parts[1];
|
||||
|
||||
// Проверяем что кнопку нажал тот же пользователь
|
||||
if (!event.getUser().getId().equals(authorId)) {
|
||||
event
|
||||
.reply("❌ Только инициатор команды может подтвердить действие!")
|
||||
.setEphemeral(true)
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonId.startsWith("reset_levels_confirm:")) {
|
||||
// Подтверждение
|
||||
if (buttonId.startsWith(PREFIX_CONFIRM)) {
|
||||
String authorId = buttonId.substring(PREFIX_CONFIRM.length());
|
||||
if (authorId.isEmpty()) return;
|
||||
if (!event.getUser().getId().equals(authorId)) {
|
||||
event.reply("❌ Только инициатор команды может подтвердить действие!").setEphemeral(true).queue();
|
||||
return;
|
||||
}
|
||||
event.deferReply().queue();
|
||||
|
||||
try {
|
||||
String guildId = event.getGuild().getId();
|
||||
String userId = event.getUser().getId();
|
||||
|
||||
// Выполняем сброс
|
||||
resetCommand.executeReset(guildId, userId);
|
||||
|
||||
// Удаляем исходное сообщение с кнопками
|
||||
resetCommand.executeReset(event.getGuild().getId(), event.getUser().getId());
|
||||
event.getMessage().delete().queue();
|
||||
|
||||
// Отправляем подтверждение
|
||||
event
|
||||
.getHook()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Сброс уровней выполнен")
|
||||
.withDescription(
|
||||
"Уровни и XP всех пользователей успешно сброшены!\n\n"
|
||||
+ "**Сохранено:**\n"
|
||||
+ "• Время в голосовых каналах\n"
|
||||
+ "• Количество сообщений\n"
|
||||
+ "• Репутация\n\n"
|
||||
+ "**Сброшено:**\n"
|
||||
+ "• Уровень → 0\n"
|
||||
+ "• XP → 0\n\n"
|
||||
+ "Теперь используется новая система уровней!")
|
||||
.withColor(Color.GREEN)
|
||||
.withFooter(
|
||||
"Инициатор: " + event.getUser().getName(), event.getUser().getAvatarUrl())
|
||||
.build())
|
||||
.queue();
|
||||
|
||||
LoggerDecorator.bot(
|
||||
logger, "Сброс уровней подтверждён пользователем: " + event.getUser().getName());
|
||||
|
||||
event.getHook().sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Сброс уровней выполнен")
|
||||
.withDescription("Уровни и XP всех пользователей успешно сброшены!\n\n"
|
||||
+ "**Сохранено:**\n• Время в голосовых каналах\n"
|
||||
+ "• Количество сообщений\n• Репутация\n\n"
|
||||
+ "**Сброшено:**\n• Уровень → 0\n• XP → 0\n\n"
|
||||
+ "Теперь используется новая система уровней!")
|
||||
.withColor(Color.GREEN)
|
||||
.withFooter("Инициатор: " + event.getUser().getName(), event.getUser().getAvatarUrl())
|
||||
.build()).queue();
|
||||
LoggerDecorator.bot(logger, "Сброс уровней подтверждён: " + event.getUser().getName());
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.error(logger, "Ошибка при выполнении сброса", e);
|
||||
event
|
||||
.getHook()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("❌ Ошибка")
|
||||
.withDescription("Произошла ошибка при сбросе уровней: " + e.getMessage())
|
||||
.withColor(Color.RED)
|
||||
.build())
|
||||
.queue();
|
||||
event.getHook().sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("❌ Ошибка")
|
||||
.withDescription("Произошла ошибка при сбросе уровней: " + e.getMessage())
|
||||
.withColor(Color.RED).build()).queue();
|
||||
}
|
||||
} else if (buttonId.startsWith(PREFIX_CANCEL)) {
|
||||
String authorId = buttonId.substring(PREFIX_CANCEL.length());
|
||||
if (authorId.isEmpty()) return;
|
||||
if (!event.getUser().getId().equals(authorId)) {
|
||||
event.reply("❌ Только инициатор команды может отменить действие!").setEphemeral(true).queue();
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (buttonId.startsWith("reset_levels_cancel:")) {
|
||||
// Отмена
|
||||
event.getMessage().delete().queue();
|
||||
event.reply("❌ Сброс уровней отменён.").setEphemeral(true).queue();
|
||||
|
||||
LoggerDecorator.bot(
|
||||
logger, "Сброс уровней отменён пользователем: " + event.getUser().getName());
|
||||
LoggerDecorator.bot(logger, "Сброс уровней отменён: " + event.getUser().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import java.util.concurrent.TimeUnit;
|
|||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import creator.loki.feature.economy.service.EconomyService;
|
||||
import creator.loki.feature.level.domain.LevelUpResult;
|
||||
import creator.loki.feature.level.service.LevelService;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
|
||||
|
|
@ -16,21 +17,22 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** Отслеживание времени в голосовых каналах */
|
||||
@Singleton
|
||||
public class VoiceTracker extends ListenerAdapter {
|
||||
private static final Logger logger = LoggerFactory.getLogger(VoiceTracker.class);
|
||||
private final LevelService levelService;
|
||||
private final EconomyService economyService;
|
||||
private final LevelUpNotifier levelUpNotifier;
|
||||
private final Map<String, Long> voiceJoinTimes = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||
private final Object lock = new Object();
|
||||
|
||||
@Inject
|
||||
public VoiceTracker(LevelService levelService, EconomyService economyService) {
|
||||
public VoiceTracker(LevelService levelService, EconomyService economyService,
|
||||
LevelUpNotifier levelUpNotifier) {
|
||||
this.levelService = levelService;
|
||||
this.economyService = economyService;
|
||||
|
||||
// Каждую минуту начисляем XP тем, кто в войсе
|
||||
this.levelUpNotifier = levelUpNotifier;
|
||||
scheduler.scheduleAtFixedRate(
|
||||
() -> Thread.startVirtualThread(this::updateVoiceTime), 1, 1, TimeUnit.MINUTES);
|
||||
}
|
||||
|
|
@ -39,66 +41,48 @@ public class VoiceTracker extends ListenerAdapter {
|
|||
public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) {
|
||||
Member member = event.getMember();
|
||||
String userId = member.getId();
|
||||
if (member.getUser().isBot()) return;
|
||||
|
||||
// Игнорируем ботов
|
||||
if (member.getUser().isBot()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Пользователь зашёл в войс
|
||||
if (event.getChannelJoined() != null && event.getChannelLeft() == null) {
|
||||
voiceJoinTimes.put(userId, System.currentTimeMillis());
|
||||
logger.debug("Пользователь {} зашёл в войс", member.getEffectiveName());
|
||||
} else if (event.getChannelLeft() != null && event.getChannelJoined() == null) {
|
||||
// Пользователь вышел из войса
|
||||
Long joinTime = voiceJoinTimes.remove(userId);
|
||||
Long joinTime;
|
||||
synchronized (lock) { joinTime = voiceJoinTimes.remove(userId); }
|
||||
if (joinTime != null) {
|
||||
long timeSpent = System.currentTimeMillis() - joinTime;
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(timeSpent);
|
||||
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - joinTime);
|
||||
if (minutes > 0) {
|
||||
levelService.addVoiceXP(userId, minutes);
|
||||
LevelUpResult lr = levelService.addVoiceXP(userId, minutes);
|
||||
economyService.earnCash(userId, minutes / 2);
|
||||
logger.debug(
|
||||
"Пользователь {} провёл в войсе {} минут", member.getEffectiveName(), minutes);
|
||||
if (lr != null && event.getMember() != null) {
|
||||
levelUpNotifier.sendLevelUpNotification(
|
||||
event.getMember(), lr.getOldLevel(), lr.getNewLevel(),
|
||||
event.getGuild().getSystemChannel());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (event.getChannelJoined() != null && event.getChannelLeft() != null) {
|
||||
// Пользователь переключился между каналами
|
||||
// Обновляем время входа
|
||||
voiceJoinTimes.put(userId, System.currentTimeMillis());
|
||||
synchronized (lock) { voiceJoinTimes.put(userId, System.currentTimeMillis()); }
|
||||
}
|
||||
}
|
||||
|
||||
/** Периодическое обновление времени для тех, кто долго в войсе */
|
||||
private void updateVoiceTime() {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
for (Map.Entry<String, Long> entry : voiceJoinTimes.entrySet()) {
|
||||
String userId = entry.getKey();
|
||||
long joinTime = entry.getValue();
|
||||
long timeSpent = now - joinTime;
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(timeSpent);
|
||||
|
||||
if (minutes >= 1) {
|
||||
// Начисляем XP за каждую минуту
|
||||
levelService.addVoiceXP(userId, 1);
|
||||
economyService.earnCash(userId, 1);
|
||||
// Обновляем время входа
|
||||
voiceJoinTimes.put(userId, now);
|
||||
synchronized (lock) {
|
||||
for (Map.Entry<String, Long> entry : voiceJoinTimes.entrySet()) {
|
||||
String userId = entry.getKey();
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(now - entry.getValue());
|
||||
if (minutes >= 1) {
|
||||
levelService.addVoiceXP(userId, 1);
|
||||
economyService.earnCash(userId, 1);
|
||||
entry.setValue(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
scheduler.shutdown();
|
||||
try {
|
||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
scheduler.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) scheduler.shutdownNow(); }
|
||||
catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ public class LevelService {
|
|||
return null;
|
||||
}
|
||||
|
||||
cooldownService.markMessageSent(userId);
|
||||
UserStats stats = repository.getUserStats(userId);
|
||||
long now = System.currentTimeMillis();
|
||||
int oldLevel = stats.getLevel();
|
||||
|
|
|
|||
|
|
@ -1,55 +1,45 @@
|
|||
package creator.loki.feature.level.service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
import creator.loki.feature.level.repository.UserStatsRepository;
|
||||
|
||||
/**
|
||||
* Сервис для управления кулдауном сообщений Использует персистентное хранение через
|
||||
* UserStats.lastMessageTime
|
||||
*/
|
||||
public class MessageCooldownService {
|
||||
private static final long MESSAGE_COOLDOWN_MS = 60000; // 1 минута
|
||||
private static final long MESSAGE_COOLDOWN_MS = 60000;
|
||||
|
||||
private final UserStatsRepository repository;
|
||||
private final Map<String, Long> lastMessageTimes = new ConcurrentHashMap<>();
|
||||
|
||||
public MessageCooldownService(UserStatsRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, может ли пользователь получить XP за сообщение
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @return true если кулдаун прошел
|
||||
*/
|
||||
public boolean canReceiveXP(String userId) {
|
||||
UserStats stats = repository.getUserStats(userId);
|
||||
long now = System.currentTimeMillis();
|
||||
long lastMessage = stats.getLastMessageTime();
|
||||
|
||||
return lastMessage == 0 || (now - lastMessage) >= MESSAGE_COOLDOWN_MS;
|
||||
Long lastTime = lastMessageTimes.get(userId);
|
||||
if (lastTime != null && (now - lastTime) < MESSAGE_COOLDOWN_MS) return false;
|
||||
if (lastTime != null) return true;
|
||||
long dbTime = repository.getUserStats(userId).getLastMessageTime();
|
||||
if (dbTime != 0) lastMessageTimes.put(userId, dbTime);
|
||||
return dbTime == 0 || (now - dbTime) >= MESSAGE_COOLDOWN_MS;
|
||||
}
|
||||
|
||||
public void markMessageSent(String userId) {
|
||||
lastMessageTimes.put(userId, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить оставшееся время кулдауна в секундах
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @return секунды до окончания кулдауна, 0 если кулдаун прошел
|
||||
*/
|
||||
public long getRemainingCooldown(String userId) {
|
||||
UserStats stats = repository.getUserStats(userId);
|
||||
long now = System.currentTimeMillis();
|
||||
long lastMessage = stats.getLastMessageTime();
|
||||
|
||||
if (lastMessage == 0) {
|
||||
return 0;
|
||||
Long lastTime = lastMessageTimes.get(userId);
|
||||
if (lastTime == null) {
|
||||
long dbTime = repository.getUserStats(userId).getLastMessageTime();
|
||||
if (dbTime == 0) return 0;
|
||||
lastMessageTimes.put(userId, dbTime);
|
||||
lastTime = dbTime;
|
||||
}
|
||||
|
||||
long elapsed = now - lastMessage;
|
||||
if (elapsed >= MESSAGE_COOLDOWN_MS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (MESSAGE_COOLDOWN_MS - elapsed) / 1000;
|
||||
long elapsed = now - lastTime;
|
||||
return elapsed >= MESSAGE_COOLDOWN_MS ? 0 : (MESSAGE_COOLDOWN_MS - elapsed) / 1000;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,16 +10,10 @@ import creator.loki.util.LoggerDecorator;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** Сборщик метрик для мониторинга бота */
|
||||
public class MetricsCollector {
|
||||
private static final Logger logger = LoggerFactory.getLogger(MetricsCollector.class);
|
||||
|
||||
// Счётчики команд
|
||||
private final ConcurrentHashMap<String, LongAdder> commandCounts = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, LongAdder> commandErrors = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, AtomicLong> commandTotalTime = new ConcurrentHashMap<>();
|
||||
|
||||
// Общие метрики
|
||||
private final ConcurrentHashMap<String, CommandEntry> commands = new ConcurrentHashMap<>();
|
||||
private final LongAdder totalCommands = new LongAdder();
|
||||
private final LongAdder totalErrors = new LongAdder();
|
||||
private final AtomicLong startTime = new AtomicLong(System.currentTimeMillis());
|
||||
|
|
@ -28,79 +22,60 @@ public class MetricsCollector {
|
|||
LoggerDecorator.bot(logger, "MetricsCollector инициализирован");
|
||||
}
|
||||
|
||||
/** Записывает выполнение команды */
|
||||
public void recordCommand(String commandName, long executionTimeMs) {
|
||||
commandCounts.computeIfAbsent(commandName, k -> new LongAdder()).increment();
|
||||
commandTotalTime.computeIfAbsent(commandName, k -> new AtomicLong()).addAndGet(executionTimeMs);
|
||||
var entry = commands.computeIfAbsent(commandName, k -> new CommandEntry());
|
||||
entry.count.increment();
|
||||
entry.totalTime.addAndGet(executionTimeMs);
|
||||
totalCommands.increment();
|
||||
}
|
||||
|
||||
/** Записывает ошибку команды */
|
||||
public void recordError(String commandName) {
|
||||
commandErrors.computeIfAbsent(commandName, k -> new LongAdder()).increment();
|
||||
var entry = commands.computeIfAbsent(commandName, k -> new CommandEntry());
|
||||
entry.errors.increment();
|
||||
totalErrors.increment();
|
||||
}
|
||||
|
||||
/** Возвращает количество выполнений команды */
|
||||
public long getCommandCount(String commandName) {
|
||||
LongAdder counter = commandCounts.get(commandName);
|
||||
return counter != null ? counter.sum() : 0;
|
||||
var entry = commands.get(commandName);
|
||||
return entry != null ? entry.count.sum() : 0;
|
||||
}
|
||||
|
||||
/** Возвращает количество ошибок команды */
|
||||
public long getCommandErrors(String commandName) {
|
||||
LongAdder counter = commandErrors.get(commandName);
|
||||
return counter != null ? counter.sum() : 0;
|
||||
var entry = commands.get(commandName);
|
||||
return entry != null ? entry.errors.sum() : 0;
|
||||
}
|
||||
|
||||
/** Возвращает среднее время выполнения команды */
|
||||
public double getAverageExecutionTime(String commandName) {
|
||||
long count = getCommandCount(commandName);
|
||||
if (count == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
AtomicLong totalTime = commandTotalTime.get(commandName);
|
||||
if (totalTime == null) {
|
||||
return 0.0;
|
||||
}
|
||||
return (double) totalTime.get() / count;
|
||||
var entry = commands.get(commandName);
|
||||
var count = entry != null ? entry.count.sum() : 0;
|
||||
return count == 0 ? 0.0 : (double) entry.totalTime.get() / count;
|
||||
}
|
||||
|
||||
/** Возвращает общее количество выполненных команд */
|
||||
public long getTotalCommands() {
|
||||
return totalCommands.sum();
|
||||
}
|
||||
|
||||
/** Возвращает общее количество ошибок */
|
||||
public long getTotalErrors() {
|
||||
return totalErrors.sum();
|
||||
}
|
||||
|
||||
/** Возвращает процент ошибок */
|
||||
public double getErrorRate() {
|
||||
long total = getTotalCommands();
|
||||
if (total == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
return (double) getTotalErrors() / total * 100;
|
||||
return getTotalCommands() == 0 ? 0.0 : (double) getTotalErrors() / getTotalCommands() * 100;
|
||||
}
|
||||
|
||||
/** Возвращает время работы бота в секундах */
|
||||
public long getUptimeSeconds() {
|
||||
return (System.currentTimeMillis() - startTime.get()) / 1000;
|
||||
}
|
||||
|
||||
/** JSON для {@code GET /health} (пробы оркестраторов). */
|
||||
public String buildHealthJson() {
|
||||
String version =
|
||||
Optional.ofNullable(MetricsCollector.class.getPackage().getImplementationVersion())
|
||||
.filter(v -> !v.isBlank())
|
||||
.orElse("dev");
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"{\"status\":\"ok\",\"version\":%s,\"uptimeSeconds\":%d,\"commandsRun\":%d,"
|
||||
+ "\"errors\":%d,\"errorRatePercent\":%.4f}",
|
||||
jsonQuoted(version),
|
||||
jsonQuoted(
|
||||
Optional.ofNullable(MetricsCollector.class.getPackage().getImplementationVersion())
|
||||
.filter(v -> !v.isBlank())
|
||||
.orElse("dev")),
|
||||
getUptimeSeconds(),
|
||||
getTotalCommands(),
|
||||
getTotalErrors(),
|
||||
|
|
@ -108,75 +83,61 @@ public class MetricsCollector {
|
|||
}
|
||||
|
||||
private static String jsonQuoted(String s) {
|
||||
StringBuilder sb = new StringBuilder(s.length() + 2);
|
||||
sb.append('"');
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '\\' -> sb.append("\\\\");
|
||||
case '"' -> sb.append("\\\"");
|
||||
case '\n' -> sb.append("\\n");
|
||||
case '\r' -> sb.append("\\r");
|
||||
case '\t' -> sb.append("\\t");
|
||||
default -> sb.append(c < ' ' ? ' ' : c);
|
||||
}
|
||||
}
|
||||
sb.append('"');
|
||||
return sb.toString();
|
||||
return '"'
|
||||
+ s.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
+ '"';
|
||||
}
|
||||
|
||||
/** Возвращает топ команд по использованию */
|
||||
public String getTopCommands(int limit) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
var sb = new StringBuilder();
|
||||
sb.append("📊 Топ команд:\n");
|
||||
|
||||
commandCounts.entrySet().stream()
|
||||
.sorted((e1, e2) -> Long.compare(e2.getValue().sum(), e1.getValue().sum()))
|
||||
commands.entrySet().stream()
|
||||
.sorted((e1, e2) -> Long.compare(e2.getValue().count.sum(), e1.getValue().count.sum()))
|
||||
.limit(limit)
|
||||
.forEach(
|
||||
entry -> {
|
||||
String cmd = entry.getKey();
|
||||
long count = entry.getValue().sum();
|
||||
long errors = getCommandErrors(cmd);
|
||||
double avgTime = getAverageExecutionTime(cmd);
|
||||
|
||||
var ce = entry.getValue();
|
||||
var count = ce.count.sum();
|
||||
var avgTime = count == 0 ? 0.0 : (double) ce.totalTime.get() / count;
|
||||
sb.append(
|
||||
String.format(
|
||||
" %s: %d выполнений, %.1f мс среднее, %d ошибок%n",
|
||||
cmd, count, avgTime, errors));
|
||||
entry.getKey(), count, avgTime, ce.errors.sum()));
|
||||
});
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** Возвращает полную статистику */
|
||||
public String getFullStats() {
|
||||
long uptime = getUptimeSeconds();
|
||||
long hours = uptime / 3600;
|
||||
long minutes = (uptime % 3600) / 60;
|
||||
long seconds = uptime % 60;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
var sb = new StringBuilder();
|
||||
sb.append("📈 Статистика бота\n");
|
||||
sb.append("━━━━━━━━━━━━━━━━━━━━\n");
|
||||
sb.append(String.format("⏱️ Uptime: %dч %dм %dс%n", hours, minutes, seconds));
|
||||
sb.append(
|
||||
String.format(
|
||||
"⏱️ Uptime: %dч %dм %dс%n",
|
||||
getUptimeSeconds() / 3600, (getUptimeSeconds() % 3600) / 60, getUptimeSeconds() % 60));
|
||||
sb.append(String.format("📝 Всего команд: %d%n", getTotalCommands()));
|
||||
sb.append(String.format("❌ Всего ошибок: %d (%.2f%%)%n", getTotalErrors(), getErrorRate()));
|
||||
sb.append(String.format("📊 Уникальных команд: %d%n", commandCounts.size()));
|
||||
sb.append(String.format("📊 Уникальных команд: %d%n", commands.size()));
|
||||
sb.append("\n");
|
||||
sb.append(getTopCommands(10));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** Сбрасывает все метрики */
|
||||
public void reset() {
|
||||
commandCounts.clear();
|
||||
commandErrors.clear();
|
||||
commandTotalTime.clear();
|
||||
commands.clear();
|
||||
totalCommands.reset();
|
||||
totalErrors.reset();
|
||||
startTime.set(System.currentTimeMillis());
|
||||
LoggerDecorator.bot(logger, "Метрики сброшены");
|
||||
}
|
||||
|
||||
private static class CommandEntry {
|
||||
final LongAdder count = new LongAdder();
|
||||
final LongAdder errors = new LongAdder();
|
||||
final AtomicLong totalTime = new AtomicLong();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import creator.loki.core.registry.CommandRegistry;
|
|||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import creator.loki.feature.config.repository.GuildConfigRepository;
|
||||
import creator.loki.feature.logging.LogSetupPrefixCommand;
|
||||
import creator.loki.feature.moderation.commands.caselookup.CaseLookupCommand;
|
||||
import creator.loki.feature.moderation.commands.punish.MuteCommand;
|
||||
import creator.loki.feature.moderation.commands.punish.PunishCommand;
|
||||
import creator.loki.feature.moderation.commands.punish.UnmuteCommand;
|
||||
import creator.loki.feature.moderation.commands.swap.SwapCommand;
|
||||
import creator.loki.feature.moderation.commands.utils.ClearCommand;
|
||||
import creator.loki.feature.moderation.commands.utils.SlowmodeCommand;
|
||||
import creator.loki.feature.moderation.commands.utils.SwapAndCaseCommand;
|
||||
import creator.loki.feature.moderation.commands.warn.UnwarnCommand;
|
||||
import creator.loki.feature.moderation.commands.warn.WarnCommand;
|
||||
import creator.loki.feature.moderation.commands.warn.WarningsHistoryCommand;
|
||||
|
|
@ -84,8 +85,8 @@ public class ModerationCommandRegistrar implements CommandRegistrar {
|
|||
registry.register(new WarningsHistoryCommand(warningRepository));
|
||||
registry.register(new ClearCommand());
|
||||
registry.register(new SlowmodeCommand());
|
||||
registry.register(SwapAndCaseCommand.createSwap());
|
||||
registry.register(SwapAndCaseCommand.createCaseLookup(moderationCaseRepository));
|
||||
registry.register(new SwapCommand());
|
||||
registry.register(new CaseLookupCommand(moderationCaseRepository));
|
||||
|
||||
registry.register(new CreateRoleCommand(roleManager));
|
||||
registry.register(new DeleteRoleCommand(roleManager));
|
||||
|
|
|
|||
|
|
@ -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 + "` на этом сервере нет"));
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@ import java.time.OffsetDateTime;
|
|||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import creator.loki.feature.moderation.util.ModerationHelper;
|
||||
import creator.loki.feature.moderation.MuteRoleService;
|
||||
import creator.loki.feature.moderation.MuteScheduler;
|
||||
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
|
||||
|
|
@ -190,10 +190,10 @@ public class MuteCommand implements Command {
|
|||
}
|
||||
|
||||
private static String sub(String s, int max) {
|
||||
return s.length() <= max ? s : s.substring(0, max - 1) + "…";
|
||||
return ModerationHelper.trunc(s, max);
|
||||
}
|
||||
|
||||
private static void err(CommandContext ctx, String msg) {
|
||||
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
|
||||
ModerationHelper.error(ctx, msg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import java.util.concurrent.TimeUnit;
|
|||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.moderation.util.ModerationHelper;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
|
@ -123,6 +124,6 @@ public final class PunishCommand {
|
|||
}
|
||||
|
||||
private static void err(CommandContext ctx, String msg) {
|
||||
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
|
||||
ModerationHelper.error(ctx, msg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import creator.loki.feature.moderation.MuteRoleService;
|
|||
import creator.loki.feature.moderation.MuteScheduler;
|
||||
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
|
||||
import creator.loki.feature.moderation.repository.ModerationCaseRepository;
|
||||
import creator.loki.feature.moderation.util.ModerationHelper;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
|
||||
|
|
@ -137,10 +138,10 @@ public class UnmuteCommand implements Command {
|
|||
}
|
||||
|
||||
private static String trunc(String s, int max) {
|
||||
return s.length() <= max ? s : s.substring(0, max - 1) + "…";
|
||||
return ModerationHelper.trunc(s, max);
|
||||
}
|
||||
|
||||
private static void err(CommandContext ctx, String msg) {
|
||||
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
|
||||
ModerationHelper.error(ctx, msg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import creator.loki.feature.moderation.MuteScheduler;
|
|||
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
|
||||
import creator.loki.feature.moderation.repository.ModerationCaseRepository;
|
||||
import creator.loki.feature.moderation.repository.WarningRepository;
|
||||
import creator.loki.feature.moderation.util.ModerationHelper;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
|
|
@ -66,38 +67,46 @@ public class WarnCommand implements Command {
|
|||
|
||||
@Override
|
||||
public void execute(CommandContext ctx) {
|
||||
var t = validateInput(ctx);
|
||||
if (t != null) saveWarning(ctx, t);
|
||||
}
|
||||
|
||||
private Member validateInput(CommandContext ctx) {
|
||||
var g = ctx.getGuild();
|
||||
if (g == null) {
|
||||
err(ctx, "Только на сервере");
|
||||
return;
|
||||
ModerationHelper.error(ctx, "Только на сервере");
|
||||
return null;
|
||||
}
|
||||
var mod = ctx.getAuthor();
|
||||
if (mod == null || !mod.hasPermission(Permission.KICK_MEMBERS)) {
|
||||
err(ctx, "Нужно **Kick Members**");
|
||||
return;
|
||||
ModerationHelper.error(ctx, "Нужно **Kick Members**");
|
||||
return null;
|
||||
}
|
||||
var mentioned = ctx.getMessage().getMentions().getMembers();
|
||||
if (mentioned.isEmpty()) {
|
||||
err(ctx, "Укажи пользователя: `" + getUsage() + "`");
|
||||
return;
|
||||
ModerationHelper.error(ctx, "Укажи пользователя: `" + getUsage() + "`");
|
||||
return null;
|
||||
}
|
||||
var t = mentioned.get(0);
|
||||
if (t.getId().equals(mod.getId())) {
|
||||
err(ctx, "Нельзя выдать варн самому себе");
|
||||
return;
|
||||
ModerationHelper.error(ctx, "Нельзя выдать варн самому себе");
|
||||
return null;
|
||||
}
|
||||
if (!mod.canInteract(t)) {
|
||||
err(ctx, "Участник выше ваших ролей");
|
||||
return;
|
||||
ModerationHelper.error(ctx, "Участник выше ваших ролей");
|
||||
return null;
|
||||
}
|
||||
var reason =
|
||||
ctx.getArgs().size() > 1
|
||||
? ctx.getArgs().stream().skip(1).collect(Collectors.joining(" ")).trim()
|
||||
: "Без причины";
|
||||
return t;
|
||||
}
|
||||
|
||||
private void saveWarning(CommandContext ctx, Member t) {
|
||||
var g = ctx.getGuild();
|
||||
var mod = ctx.getAuthor();
|
||||
var reason = ctx.getArgs().stream().skip(1).collect(Collectors.joining(" ")).trim();
|
||||
if (reason.isEmpty()) reason = "Без причины";
|
||||
long id = warnings.insert(g.getId(), t.getId(), mod.getId(), reason);
|
||||
if (id < 0) {
|
||||
err(ctx, "Не удалось сохранить предупреждение");
|
||||
ModerationHelper.error(ctx, "Не удалось сохранить предупреждение");
|
||||
return;
|
||||
}
|
||||
int total = warnings.countByUser(g.getId(), t.getId());
|
||||
|
|
@ -110,7 +119,7 @@ public class WarnCommand implements Command {
|
|||
+ "\n**ID записи:** `"
|
||||
+ id
|
||||
+ "`\n**Причина:** "
|
||||
+ trunc(reason, 500)
|
||||
+ ModerationHelper.trunc(reason, 500)
|
||||
+ "\n**Всего варнов:** "
|
||||
+ total))
|
||||
.queue();
|
||||
|
|
@ -133,7 +142,7 @@ public class WarnCommand implements Command {
|
|||
.addField("Модератор", mod.getAsMention(), false)
|
||||
.addField("ID записи", "`" + warnId + "`", true)
|
||||
.addField("Всего варнов", String.valueOf(total), true)
|
||||
.addField("Причина", trunc(reason, 1000), false)
|
||||
.addField("Причина", ModerationHelper.trunc(reason, 1000), false)
|
||||
.setTimestamp(OffsetDateTime.now())
|
||||
.build())
|
||||
.queue(s -> {}, e -> {});
|
||||
|
|
@ -148,7 +157,7 @@ public class WarnCommand implements Command {
|
|||
}
|
||||
var self = g.getMember(g.getJDA().getSelfUser());
|
||||
if (!self.canInteract(t) || !self.canInteract(muteRole)) return;
|
||||
var reason = "Авто-мут: 3+ варна";
|
||||
String reason = "Авто-мут: 3+ варна";
|
||||
long untilMs = System.currentTimeMillis() + AUTO_MUTE_HOURS * 3600 * 1000;
|
||||
long durSec = AUTO_MUTE_HOURS * 3600;
|
||||
long caseId = cases.insert(g.getId(), "MUTE", t.getId(), mod.getId(), reason, durSec, untilMs);
|
||||
|
|
@ -173,12 +182,4 @@ public class WarnCommand implements Command {
|
|||
},
|
||||
err -> activeMutes.delete(g.getId(), t.getId()));
|
||||
}
|
||||
|
||||
private static String trunc(String s, int max) {
|
||||
return s.length() <= max ? s : s.substring(0, max - 1) + "…";
|
||||
}
|
||||
|
||||
private static void err(CommandContext ctx, String msg) {
|
||||
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import net.dv8tion.jda.api.EmbedBuilder;
|
|||
import net.dv8tion.jda.api.audit.ActionType;
|
||||
import net.dv8tion.jda.api.audit.AuditLogEntry;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
import net.dv8tion.jda.api.events.guild.GuildBanEvent;
|
||||
import net.dv8tion.jda.api.events.guild.GuildUnbanEvent;
|
||||
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
|
||||
|
|
@ -35,165 +34,118 @@ public class ModerationLogger extends ListenerAdapter {
|
|||
}
|
||||
|
||||
private void handle(
|
||||
Guild guild, ActionType type, int limit, Function<List<AuditLogEntry>, EmbedBuilder> fn) {
|
||||
var log = getLogChannel(guild);
|
||||
Guild g, ActionType t, int l, Function<List<AuditLogEntry>, EmbedBuilder> fn) {
|
||||
var log = g.getTextChannelById(configRepo.get("moderation.log_channel_id"));
|
||||
if (log == null) return;
|
||||
guild
|
||||
.retrieveAuditLogs()
|
||||
.type(type)
|
||||
.limit(limit)
|
||||
.queue(
|
||||
logs -> {
|
||||
var embed = fn.apply(logs);
|
||||
if (embed != null) {
|
||||
log.sendMessageEmbeds(embed.setTimestamp(OffsetDateTime.now()).build()).queue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onMessageDelete(MessageDeleteEvent event) {
|
||||
var content = messageCache.getContent(event.getMessageId());
|
||||
handle(
|
||||
event.getGuild(),
|
||||
ActionType.MESSAGE_DELETE,
|
||||
1,
|
||||
var al = g.retrieveAuditLogs().type(t).limit(l);
|
||||
al.queue(
|
||||
logs -> {
|
||||
var entry = logs.isEmpty() ? null : logs.get(0);
|
||||
return LogEmbeds.messageDelete(entry, event.getChannel().getAsMention(), content);
|
||||
var embed = fn.apply(logs);
|
||||
if (embed == null) return;
|
||||
log.sendMessageEmbeds(embed.setTimestamp(OffsetDateTime.now()).build()).queue();
|
||||
});
|
||||
}
|
||||
|
||||
public void onMessageBulkDelete(MessageBulkDeleteEvent event) {
|
||||
handle(
|
||||
event.getGuild(),
|
||||
ActionType.MESSAGE_BULK_DELETE,
|
||||
1,
|
||||
logs -> {
|
||||
var entry = logs.isEmpty() ? null : logs.get(0);
|
||||
return LogEmbeds.messageBulkDelete(
|
||||
entry, event.getChannel().getAsMention(), event.getMessageIds().size());
|
||||
});
|
||||
public void onMessageDelete(MessageDeleteEvent e) {
|
||||
var content = messageCache.getContent(e.getMessageId());
|
||||
handle(e.getGuild(), ActionType.MESSAGE_DELETE, 1, l -> md(e, content, l));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildBan(GuildBanEvent event) {
|
||||
handle(
|
||||
event.getGuild(),
|
||||
ActionType.BAN,
|
||||
1,
|
||||
logs -> {
|
||||
if (logs.isEmpty()) return null;
|
||||
var entry = logs.get(0);
|
||||
var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно";
|
||||
var reason = entry.getReason() != null ? entry.getReason() : "Не указана";
|
||||
return LogEmbeds.ban(
|
||||
event.getUser().getAsMention(), mod, reason, event.getUser().getAvatarUrl());
|
||||
});
|
||||
private EmbedBuilder md(MessageDeleteEvent e, String c, List<AuditLogEntry> l) {
|
||||
var entry = l.isEmpty() ? null : l.get(0);
|
||||
return LogEmbeds.messageDelete(entry, e.getChannel().getAsMention(), c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildUnban(GuildUnbanEvent event) {
|
||||
handle(
|
||||
event.getGuild(),
|
||||
ActionType.UNBAN,
|
||||
1,
|
||||
logs -> {
|
||||
if (logs.isEmpty()) return null;
|
||||
var entry = logs.get(0);
|
||||
var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно";
|
||||
return LogEmbeds.unban(
|
||||
event.getUser().getAsMention(), mod, event.getUser().getAvatarUrl());
|
||||
});
|
||||
public void onMessageBulkDelete(MessageBulkDeleteEvent e) {
|
||||
handle(e.getGuild(), ActionType.MESSAGE_BULK_DELETE, 1, l -> mbd(e, l));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildMemberRemove(GuildMemberRemoveEvent event) {
|
||||
handle(
|
||||
event.getGuild(),
|
||||
ActionType.KICK,
|
||||
1,
|
||||
logs -> {
|
||||
if (logs.isEmpty()) return null;
|
||||
var entry = logs.get(0);
|
||||
if (!entry.getTargetId().equals(event.getUser().getId())) return null;
|
||||
var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно";
|
||||
var reason = entry.getReason() != null ? entry.getReason() : "Не указана";
|
||||
return LogEmbeds.kick(
|
||||
event.getUser().getAsMention(), mod, reason, event.getUser().getAvatarUrl());
|
||||
});
|
||||
private EmbedBuilder mbd(MessageBulkDeleteEvent e, List<AuditLogEntry> l) {
|
||||
var entry = l.isEmpty() ? null : l.get(0);
|
||||
var size = e.getMessageIds().size();
|
||||
return LogEmbeds.messageBulkDelete(entry, e.getChannel().getAsMention(), size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildMemberUpdateTimeOut(GuildMemberUpdateTimeOutEvent event) {
|
||||
var muted = event.getNewTimeOutEnd() != null;
|
||||
handle(
|
||||
event.getGuild(),
|
||||
ActionType.MEMBER_UPDATE,
|
||||
5,
|
||||
logs -> {
|
||||
var entry =
|
||||
logs.stream()
|
||||
.filter(e -> event.getUser().getId().equals(e.getTargetId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (entry == null) return null;
|
||||
var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно";
|
||||
var end = muted ? event.getNewTimeOutEnd().toEpochSecond() : null;
|
||||
return LogEmbeds.timeout(
|
||||
event.getUser().getAsMention(), mod, muted, end, entry.getReason());
|
||||
});
|
||||
public void onGuildBan(GuildBanEvent ev) {
|
||||
handle(ev.getGuild(), ActionType.BAN, 1, l -> b(ev, l));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildVoiceGuildMute(GuildVoiceGuildMuteEvent event) {
|
||||
var muted = event.isGuildMuted();
|
||||
handle(
|
||||
event.getGuild(),
|
||||
ActionType.MEMBER_UPDATE,
|
||||
5,
|
||||
logs -> {
|
||||
var entry =
|
||||
logs.stream()
|
||||
.filter(e -> event.getMember().getId().equals(e.getTargetId()))
|
||||
.filter(e -> e.getChangeByKey("mute") != null)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
var mod =
|
||||
entry != null && entry.getUser() != null ? entry.getUser().getAsMention() : null;
|
||||
return LogEmbeds.voiceMute(
|
||||
event.getMember().getAsMention(),
|
||||
event.getVoiceState().getChannel().getAsMention(),
|
||||
muted,
|
||||
mod);
|
||||
});
|
||||
private EmbedBuilder b(GuildBanEvent ev, List<AuditLogEntry> l) {
|
||||
if (l.isEmpty()) return null;
|
||||
var user = ev.getUser();
|
||||
var e = l.get(0);
|
||||
var mod = e.getUser() != null ? e.getUser().getAsMention() : "Неизвестно";
|
||||
var reason = e.getReason() != null ? e.getReason() : "Не указана";
|
||||
return LogEmbeds.ban(user.getAsMention(), mod, reason, user.getAvatarUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildVoiceGuildDeafen(GuildVoiceGuildDeafenEvent event) {
|
||||
var deaf = event.isGuildDeafened();
|
||||
handle(
|
||||
event.getGuild(),
|
||||
ActionType.MEMBER_UPDATE,
|
||||
5,
|
||||
logs -> {
|
||||
var entry =
|
||||
logs.stream()
|
||||
.filter(e -> event.getMember().getId().equals(e.getTargetId()))
|
||||
.filter(e -> e.getChangeByKey("deaf") != null)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
var mod =
|
||||
entry != null && entry.getUser() != null ? entry.getUser().getAsMention() : null;
|
||||
return LogEmbeds.voiceDeafen(
|
||||
event.getMember().getAsMention(),
|
||||
event.getVoiceState().getChannel().getAsMention(),
|
||||
deaf,
|
||||
mod);
|
||||
});
|
||||
public void onGuildUnban(GuildUnbanEvent ev) {
|
||||
handle(ev.getGuild(), ActionType.UNBAN, 1, l -> ub(ev, l));
|
||||
}
|
||||
|
||||
private TextChannel getLogChannel(Guild guild) {
|
||||
var logChannelId = configRepo.get("moderation.log_channel_id");
|
||||
return logChannelId != null ? guild.getTextChannelById(logChannelId) : null;
|
||||
private EmbedBuilder ub(GuildUnbanEvent ev, List<AuditLogEntry> l) {
|
||||
if (l.isEmpty()) return null;
|
||||
var user = ev.getUser();
|
||||
var e = l.get(0);
|
||||
var mod = e.getUser() != null ? e.getUser().getAsMention() : "Неизвестно";
|
||||
return LogEmbeds.unban(user.getAsMention(), mod, user.getAvatarUrl());
|
||||
}
|
||||
|
||||
public void onGuildMemberRemove(GuildMemberRemoveEvent ev) {
|
||||
handle(ev.getGuild(), ActionType.KICK, 1, l -> k(ev, l));
|
||||
}
|
||||
|
||||
private EmbedBuilder k(GuildMemberRemoveEvent ev, List<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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +161,10 @@ public class FilterCommands {
|
|||
public void execute(CommandContext ctx) {
|
||||
var m = mgr(pm, ctx);
|
||||
if (!checkChannel(m, ctx)) return;
|
||||
boolean enabled = isNight ? !m.getScheduler().isNightcore() : !m.getScheduler().isVaporwave();
|
||||
boolean enabled =
|
||||
isNight
|
||||
? !m.getScheduler().filterState.isNightcore()
|
||||
: !m.getScheduler().filterState.isVaporwave();
|
||||
if (isNight) m.getScheduler().setNightcore(enabled);
|
||||
else m.getScheduler().setVaporwave(enabled);
|
||||
ctx.getChannel()
|
||||
|
|
|
|||
|
|
@ -1,207 +1,149 @@
|
|||
package creator.loki.feature.music.commands;
|
||||
|
||||
import static creator.loki.embed.EmbedTemplates.error;
|
||||
import static creator.loki.embed.EmbedTemplates.info;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class QueueCommands {
|
||||
private static final Color C = new Color(88, 101, 242);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(QueueCommands.class);
|
||||
|
||||
private QueueCommands() {}
|
||||
|
||||
public static Command queue(PlayerManager pm) {
|
||||
return new Queue(pm);
|
||||
return new Mc(pm, "queue", "Показать очередь треков", null, null, QueueCommands::executeQueue);
|
||||
}
|
||||
|
||||
public static Command nowplaying(PlayerManager pm) {
|
||||
return new NowPlaying(pm);
|
||||
BiConsumer<PlayerManager, CommandContext> exec = QueueCommands::executeNowPlaying;
|
||||
return new Mc(pm, "nowplaying", "Показать текущий трек", "nowplaying", "Музыка", exec);
|
||||
}
|
||||
|
||||
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 final PlayerManager pm;
|
||||
private static final class Mc implements Command {
|
||||
private final PlayerManager p;
|
||||
private final String n, d, u, c;
|
||||
private final BiConsumer<PlayerManager, CommandContext> e;
|
||||
|
||||
Queue(PlayerManager pm) {
|
||||
this.pm = pm;
|
||||
Mc(
|
||||
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() {
|
||||
return "queue";
|
||||
return n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Показать очередь треков";
|
||||
return d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandContext ctx) {
|
||||
var m = pm.getMusicManager(ctx.getGuild().getIdLong());
|
||||
Track cur = m.getScheduler().getCurrentTrack();
|
||||
List<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() {
|
||||
return "nowplaying";
|
||||
return u != null ? u : n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "Музыка";
|
||||
return c != null ? c : "Общие";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandContext ctx) {
|
||||
var lm = pm.getLavalinkManager();
|
||||
var track = lm.getMusicManager(ctx.getGuild().getIdLong()).getScheduler().getCurrentTrack();
|
||||
if (track == null) {
|
||||
ctx.getChannel().sendMessage("Сейчас ничего не играет.").queue();
|
||||
return;
|
||||
}
|
||||
var link = lm.getClient().getLinkIfCached(ctx.getGuild().getIdLong());
|
||||
long pos =
|
||||
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getPosition() : 0;
|
||||
long dur = track.getInfo().getLength();
|
||||
long ps = pos / 1000;
|
||||
long ds = dur / 1000;
|
||||
if (ds <= 0) ds = 1;
|
||||
int filled = (int) ((double) pos / dur * 20);
|
||||
StringBuilder bar = new StringBuilder("\u2503");
|
||||
for (int i = 0; i < 20; i++) bar.append(i < filled ? "\u2588" : "\u2591");
|
||||
String pbar =
|
||||
bar
|
||||
+ "\u2503 "
|
||||
+ String.format("%d:%02d", ps / 60, ps % 60)
|
||||
+ " / "
|
||||
+ String.format("%d:%02d", ds / 60, ds % 60);
|
||||
e.accept(p, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private static void executeQueue(PlayerManager pm, CommandContext ctx) {
|
||||
var m = pm.getMusicManager(ctx.getGuild().getIdLong());
|
||||
var cur = m.getScheduler().getCurrentTrack();
|
||||
var q = m.getScheduler().getQueue();
|
||||
if (cur == null && q.isEmpty()) {
|
||||
ctx.getChannel().sendMessage("Очередь пуста.").queue();
|
||||
return;
|
||||
}
|
||||
var embed = EmbedFactory.create().withTitle("Очередь треков").withColor(C);
|
||||
if (cur != null) {
|
||||
var i = cur.getInfo();
|
||||
embed.addField("Сейчас играет", "**" + i.getTitle() + "**\n" + i.getAuthor(), false);
|
||||
}
|
||||
if (!q.isEmpty()) {
|
||||
var sb = new StringBuilder();
|
||||
var show = Math.min(q.size(), 10);
|
||||
for (var i = 0; i < show; i++)
|
||||
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()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("Сейчас играет")
|
||||
.withColor(new Color(88, 101, 242))
|
||||
.addField("Название", "**" + track.getInfo().getTitle() + "**", false)
|
||||
.addField("Автор", track.getInfo().getAuthor(), true)
|
||||
.addField("Длительность", pbar, true)
|
||||
.build())
|
||||
.sendMessageEmbeds(info("Громкость", "Установлена на **" + vol + "%**"))
|
||||
.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();
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
ctx.getChannel().sendMessageEmbeds(error("Аргументы", "Нужно число от 0 до 100.")).queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ public class LavalinkManager {
|
|||
event -> {
|
||||
GuildMusicManager manager = musicManagers.get(event.getGuildId());
|
||||
if (manager != null) {
|
||||
manager.getScheduler().onTrackStart(event.getTrack());
|
||||
manager.cancelAutoLeaveTimer();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,20 +3,21 @@ package creator.loki.feature.music.listener;
|
|||
import java.util.List;
|
||||
|
||||
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.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class MusicBotControlHandler {
|
||||
private final LavalinkManager lavalinkManager;
|
||||
private final PlayerManager playerManager;
|
||||
private static final Logger log = LoggerFactory.getLogger(MusicBotControlHandler.class);
|
||||
private static final int ERR = 0xFF4444;
|
||||
|
||||
public MusicBotControlHandler(LavalinkManager lavalinkManager, PlayerManager playerManager) {
|
||||
private final LavalinkManager lavalinkManager;
|
||||
|
||||
public MusicBotControlHandler(LavalinkManager lavalinkManager) {
|
||||
this.lavalinkManager = lavalinkManager;
|
||||
this.playerManager = playerManager;
|
||||
}
|
||||
|
||||
public void handleSkip(MessageChannel ch, Guild guild, Member member) {
|
||||
|
|
@ -26,7 +27,7 @@ public class MusicBotControlHandler {
|
|||
send(ch, err("Нечего пропускать."));
|
||||
return;
|
||||
}
|
||||
String t = s.getCurrentTrack().getInfo().getTitle();
|
||||
var t = s.getCurrentTrack().getInfo().getTitle();
|
||||
s.skip();
|
||||
send(ch, ok("⏭ Пропущено", "**" + t + "**"));
|
||||
}
|
||||
|
|
@ -35,7 +36,7 @@ public class MusicBotControlHandler {
|
|||
if (reqVoice(ch, member) == null) return;
|
||||
lavalinkManager.getMusicManager(guild.getIdLong()).stop();
|
||||
guild.getJDA().getDirectAudioController().disconnect(guild);
|
||||
send(ch, ok(0xFF4444, "⏹ Остановлено", "Музыка остановлена, очередь очищена."));
|
||||
send(ch, ok(ERR, "⏹ Остановлено", "Музыка остановлена, очередь очищена."));
|
||||
}
|
||||
|
||||
public void handleQueue(MessageChannel ch, Guild guild) {
|
||||
|
|
@ -47,18 +48,15 @@ public class MusicBotControlHandler {
|
|||
return;
|
||||
}
|
||||
var sb = new StringBuilder();
|
||||
if (cur != null) {
|
||||
sb.append("**Сейчас:** ").append(cur.getInfo().getTitle()).append("\n");
|
||||
}
|
||||
if (cur != null) sb.append("**Сейчас:** ").append(cur.getInfo().getTitle()).append("\n");
|
||||
if (!q.isEmpty()) {
|
||||
int show = Math.min(q.size(), 10);
|
||||
for (int i = 0; i < show; i++) {
|
||||
var show = Math.min(q.size(), 10);
|
||||
for (var 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("** треков");
|
||||
}
|
||||
send(ch, ok("📜 Очередь", sb.toString()));
|
||||
|
|
@ -79,22 +77,23 @@ public class MusicBotControlHandler {
|
|||
if (reqVoice(ch, member) == null) return;
|
||||
var link = lavalinkManager.getClient().getLinkIfCached(guild.getIdLong());
|
||||
if (args.isEmpty()) {
|
||||
int v =
|
||||
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getVolume() : 100;
|
||||
send(ch, ok("🔊 Громкость", "Текущая: **" + v + "%**"));
|
||||
send(
|
||||
ch,
|
||||
ok(
|
||||
"🔊 Громкость",
|
||||
"Текущая: **"
|
||||
+ (link != null && link.getCachedPlayer() != null
|
||||
? link.getCachedPlayer().getVolume()
|
||||
: 100)
|
||||
+ "%**"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
int vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0))));
|
||||
if (link != null && link.getCachedPlayer() != null) {
|
||||
var vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0))));
|
||||
if (link != null && link.getCachedPlayer() != null)
|
||||
link.createOrUpdatePlayer()
|
||||
.setVolume(vol)
|
||||
.subscribe(
|
||||
null,
|
||||
err ->
|
||||
LoggerFactory.getLogger(MusicBotControlHandler.class)
|
||||
.warn("Lavalink: {}", err.getMessage()));
|
||||
}
|
||||
.subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage()));
|
||||
send(ch, ok("🔊 Громкость", "Установлена **" + vol + "%**"));
|
||||
} catch (NumberFormatException e) {
|
||||
send(ch, err("Укажи число от 0 до 100."));
|
||||
|
|
@ -105,17 +104,19 @@ public class MusicBotControlHandler {
|
|||
if (reqVoice(ch, member) == null) return;
|
||||
lavalinkManager.getMusicManager(guild.getIdLong()).stop();
|
||||
guild.getJDA().getDirectAudioController().disconnect(guild);
|
||||
send(ch, ok(0xFF4444, "👋 Отключился", "Вышел из голосового канала."));
|
||||
send(ch, ok(ERR, "👋 Отключился", "Вышел из голосового канала."));
|
||||
}
|
||||
|
||||
public void handleRepeat(MessageChannel ch, Guild guild, Member member) {
|
||||
if (reqVoice(ch, member) == null) return;
|
||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||
boolean now = !s.isRepeating();
|
||||
var now = !s.isRepeating();
|
||||
s.setRepeating(now);
|
||||
String t = now ? "🔁 Повтор включён" : "➡ Повтор выключен";
|
||||
String d = now ? "Очередь будет повторяться." : "Повтор отключён.";
|
||||
send(ch, ok(t, d));
|
||||
send(
|
||||
ch,
|
||||
ok(
|
||||
now ? "🔁 Повтор включён" : "➡ Повтор выключен",
|
||||
now ? "Очередь будет повторяться." : "Повтор отключён."));
|
||||
}
|
||||
|
||||
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) {
|
||||
if (reqVoice(ch, member) == null) return;
|
||||
int level = 10;
|
||||
if (!args.isEmpty()) {
|
||||
var level = 10;
|
||||
if (!args.isEmpty())
|
||||
try {
|
||||
level = Math.max(-25, Math.min(25, Integer.parseInt(args.get(0))));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler().setBassboost(level);
|
||||
send(ch, ok("🔊 Bassboost", "**" + level + "**"));
|
||||
}
|
||||
|
|
@ -145,7 +145,7 @@ public class MusicBotControlHandler {
|
|||
public void handleNightcore(MessageChannel ch, Guild guild, Member member) {
|
||||
if (reqVoice(ch, member) == null) return;
|
||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||
boolean now = !s.isNightcore();
|
||||
var now = !s.filterState.isNightcore();
|
||||
s.setNightcore(now);
|
||||
send(ch, ok("🎵 Nightcore", now ? "Включён ⚡" : "Выключен"));
|
||||
}
|
||||
|
|
@ -153,7 +153,7 @@ public class MusicBotControlHandler {
|
|||
public void handleVaporwave(MessageChannel ch, Guild guild, Member member) {
|
||||
if (reqVoice(ch, member) == null) return;
|
||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||
boolean now = !s.isVaporwave();
|
||||
var now = !s.filterState.isVaporwave();
|
||||
s.setVaporwave(now);
|
||||
send(ch, ok("🎵 Vaporwave", now ? "Включён 🌊" : "Выключен"));
|
||||
}
|
||||
|
|
@ -171,10 +171,12 @@ public class MusicBotControlHandler {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
int 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 sc = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||
sc.setEqualizerBand(band, gain);
|
||||
var band = Math.max(0, Math.min(14, Integer.parseInt(args.get(0))));
|
||||
var gain = (float) Math.max(-0.25, Math.min(1.0, Double.parseDouble(args.get(1))));
|
||||
lavalinkManager
|
||||
.getMusicManager(guild.getIdLong())
|
||||
.getScheduler()
|
||||
.setEqualizerBand(band, gain);
|
||||
send(
|
||||
ch,
|
||||
ok("🎛 Эквалайзер", "Band **" + band + "** → **" + String.format("%.2f", gain) + "**"));
|
||||
|
|
@ -202,6 +204,6 @@ public class MusicBotControlHandler {
|
|||
}
|
||||
|
||||
private static EmbedBuilder err(String msg) {
|
||||
return new EmbedBuilder().setColor(0xFF4444).setDescription("❌ " + msg);
|
||||
return new EmbedBuilder().setColor(ERR).setDescription("❌ " + msg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public class MusicBotListener extends ListenerAdapter {
|
|||
QueueRepository queueRepository,
|
||||
creator.loki.feature.music.core.PlayerManager playerManager) {
|
||||
this.playHandler = new MusicBotPlayHandler(lavalinkManager);
|
||||
this.controlHandler = new MusicBotControlHandler(lavalinkManager, playerManager);
|
||||
this.controlHandler = new MusicBotControlHandler(lavalinkManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -34,34 +34,39 @@ public class FilterState {
|
|||
return vaporwave;
|
||||
}
|
||||
|
||||
public void setBassboost(int level) {
|
||||
public FilterState setBassboost(int level) {
|
||||
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;
|
||||
if (enabled) this.vaporwave = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setVaporwave(boolean enabled) {
|
||||
public FilterState setVaporwave(boolean enabled) {
|
||||
this.vaporwave = enabled;
|
||||
if (enabled) this.nightcore = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setEqualizerBand(int band, float gain) {
|
||||
if (band < 0 || band > 14) return;
|
||||
public FilterState setEqualizerBand(int band, float gain) {
|
||||
if (band < 0 || band > 14) return this;
|
||||
customBands.removeIf(b -> b.getBand() == band);
|
||||
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.nightcore = false;
|
||||
this.vaporwave = false;
|
||||
this.customBands.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
public void apply(Optional<Link> link) {
|
||||
public FilterState apply(Optional<Link> link) {
|
||||
link.ifPresent(
|
||||
l -> {
|
||||
FilterBuilder fb = new FilterBuilder();
|
||||
|
|
@ -86,5 +91,6 @@ public class FilterState {
|
|||
.setFilters(fb.build())
|
||||
.subscribe(null, err -> System.err.println("Filter error: " + err.getMessage()));
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.QueueTrack;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import dev.arbjerg.lavalink.client.LavalinkNode;
|
||||
import dev.arbjerg.lavalink.client.Link;
|
||||
import dev.arbjerg.lavalink.client.player.LavalinkPlayer;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
|
|
@ -27,7 +28,6 @@ public class GuildMusicManager {
|
|||
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
|
||||
private ScheduledFuture<?> autoLeaveFuture;
|
||||
|
||||
@SuppressWarnings("this-escape")
|
||||
public GuildMusicManager(
|
||||
long guildId, LavalinkManager lavalinkManager, QueueRepository queueRepository) {
|
||||
this.guildId = guildId;
|
||||
|
|
@ -62,20 +62,17 @@ public class GuildMusicManager {
|
|||
|
||||
public void startAutoLeaveTimer() {
|
||||
cancelAutoLeaveTimer();
|
||||
autoLeaveFuture =
|
||||
executor.schedule(
|
||||
() -> {
|
||||
stop();
|
||||
getLink()
|
||||
.ifPresent(
|
||||
link ->
|
||||
link.destroy()
|
||||
.subscribe(
|
||||
null, err -> logger.warn("Lavalink: {}", err.getMessage())));
|
||||
lavalinkManager.removeMusicManager(guildId);
|
||||
},
|
||||
2,
|
||||
TimeUnit.MINUTES);
|
||||
autoLeaveFuture = executor.schedule(this::autoLeave, 2, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
private void autoLeave() {
|
||||
stop();
|
||||
getLink()
|
||||
.ifPresent(
|
||||
link ->
|
||||
link.destroy()
|
||||
.subscribe(null, err -> logger.warn("Lavalink: {}", err.getMessage())));
|
||||
lavalinkManager.removeMusicManager(guildId);
|
||||
}
|
||||
|
||||
public void cancelAutoLeaveTimer() {
|
||||
|
|
@ -101,43 +98,33 @@ public class GuildMusicManager {
|
|||
return guildId;
|
||||
}
|
||||
|
||||
QueueRepository getQueueRepo() {
|
||||
return queueRepository;
|
||||
}
|
||||
|
||||
private void loadQueueAsync() {
|
||||
List<QueueTrack> saved = queueRepository.loadQueue(String.valueOf(guildId));
|
||||
var saved = queueRepository.loadQueue(String.valueOf(guildId));
|
||||
if (saved.isEmpty()) return;
|
||||
|
||||
var client = lavalinkManager.getClient();
|
||||
if (client == null || client.getNodes().isEmpty()) return;
|
||||
var node = client.getNodes().get(0);
|
||||
executor.execute(() -> restoreTracks(saved, client.getNodes().get(0)));
|
||||
}
|
||||
|
||||
executor.execute(
|
||||
() -> {
|
||||
List<Track> decoded = new ArrayList<>();
|
||||
for (QueueTrack qt : saved) {
|
||||
try {
|
||||
Track track = node.decodeTrack(qt.encoded()).block();
|
||||
if (track != null) decoded.add(track);
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.warn(logger, "Не удалось декодировать: " + qt.title());
|
||||
}
|
||||
}
|
||||
if (decoded.isEmpty()) return;
|
||||
var q = scheduler.getRawQueue();
|
||||
q.addAll(decoded.subList(1, decoded.size()));
|
||||
Track first = decoded.get(0);
|
||||
getPlayer()
|
||||
.ifPresentOrElse(
|
||||
player -> {
|
||||
if (player.getTrack() == null) scheduler.startTrack(first);
|
||||
else q.offer(first);
|
||||
},
|
||||
() -> q.offer(first));
|
||||
LoggerDecorator.music(logger, "Восстановлено " + decoded.size() + " треков");
|
||||
queueRepository.deleteQueue(String.valueOf(guildId));
|
||||
});
|
||||
private void restoreTracks(List<QueueTrack> saved, LavalinkNode node) {
|
||||
var decoded = new ArrayList<Track>();
|
||||
for (var qt : saved) {
|
||||
try {
|
||||
var track = node.decodeTrack(qt.encoded()).block();
|
||||
if (track != null) decoded.add(track);
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.warn(logger, "Не удалось декодировать: " + qt.title());
|
||||
}
|
||||
}
|
||||
if (decoded.isEmpty()) return;
|
||||
var q = scheduler.queue;
|
||||
q.addAll(decoded.subList(1, decoded.size()));
|
||||
var first = decoded.get(0);
|
||||
var player = getPlayer().orElse(null);
|
||||
if (player != null && player.getTrack() == null) scheduler.startTrack(first);
|
||||
else q.offer(first);
|
||||
LoggerDecorator.music(logger, "Восстановлено " + decoded.size() + " треков");
|
||||
queueRepository.deleteQueue(String.valueOf(guildId));
|
||||
}
|
||||
|
||||
private ScheduledFuture<?> saveDebounced;
|
||||
|
|
@ -148,20 +135,16 @@ public class GuildMusicManager {
|
|||
}
|
||||
|
||||
void saveQueue() {
|
||||
Track current = scheduler.getCurrentTrack();
|
||||
List<Track> tracks = new ArrayList<>(scheduler.getQueue());
|
||||
List<QueueTrack> queueTracks = new ArrayList<>();
|
||||
var current = scheduler.getCurrentTrack();
|
||||
var tracks = new ArrayList<>(scheduler.getQueue());
|
||||
var queueTracks = new ArrayList<QueueTrack>();
|
||||
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);
|
||||
}
|
||||
|
||||
private static QueueTrack toQueueTrack(Track t) {
|
||||
return new QueueTrack(
|
||||
t.getEncoded(),
|
||||
t.getInfo().getTitle(),
|
||||
t.getInfo().getAuthor(),
|
||||
t.getInfo().getUri(),
|
||||
t.getInfo().getLength());
|
||||
var i = t.getInfo();
|
||||
return new QueueTrack(t.getEncoded(), i.getTitle(), i.getAuthor(), i.getUri(), i.getLength());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import dev.arbjerg.lavalink.client.Link;
|
||||
|
|
@ -17,45 +18,32 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
public class TrackScheduler {
|
||||
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 Queue<Track> queue;
|
||||
final Queue<Track> queue = new LinkedList<>();
|
||||
private boolean repeating;
|
||||
private final FilterState filterState;
|
||||
public final FilterState filterState = new FilterState();
|
||||
|
||||
public TrackScheduler(GuildMusicManager manager) {
|
||||
this.manager = manager;
|
||||
this.queue = new LinkedList<>();
|
||||
this.repeating = false;
|
||||
this.filterState = new FilterState();
|
||||
}
|
||||
|
||||
public void enqueue(Track track) {
|
||||
manager.cancelAutoLeaveTimer();
|
||||
manager
|
||||
.getPlayer()
|
||||
.ifPresentOrElse(
|
||||
player -> {
|
||||
if (player.getTrack() == null) startTrack(track);
|
||||
else {
|
||||
queue.offer(track);
|
||||
manager.saveQueueDebounced();
|
||||
}
|
||||
},
|
||||
() -> startTrack(track));
|
||||
if (manager.getPlayer().filter(p -> p.getTrack() != null).isPresent()) {
|
||||
queue.offer(track);
|
||||
manager.saveQueueDebounced();
|
||||
} else {
|
||||
startTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
public void skip() {
|
||||
manager
|
||||
.getPlayer()
|
||||
.ifPresent(
|
||||
player ->
|
||||
player
|
||||
.setTrack(null)
|
||||
.subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage())));
|
||||
manager.getPlayer().ifPresent(p -> p.setTrack(null).subscribe(null, LOG_LAV));
|
||||
}
|
||||
|
||||
public boolean nextTrack() {
|
||||
Track next = queue.poll();
|
||||
var next = queue.poll();
|
||||
if (next == null) return false;
|
||||
startTrack(next);
|
||||
manager.saveQueueDebounced();
|
||||
|
|
@ -65,19 +53,12 @@ public class TrackScheduler {
|
|||
public void stop() {
|
||||
manager.cancelAutoLeaveTimer();
|
||||
queue.clear();
|
||||
manager
|
||||
.getPlayer()
|
||||
.ifPresent(
|
||||
player ->
|
||||
player
|
||||
.setPaused(false)
|
||||
.setTrack(null)
|
||||
.subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage())));
|
||||
manager.getPlayer().ifPresent(p -> p.setPaused(false).setTrack(null).subscribe(null, LOG_LAV));
|
||||
manager.saveQueue();
|
||||
}
|
||||
|
||||
public void shuffle() {
|
||||
List<Track> list = new ArrayList<>(queue);
|
||||
var list = new ArrayList<>(queue);
|
||||
Collections.shuffle(list);
|
||||
queue.clear();
|
||||
queue.addAll(list);
|
||||
|
|
@ -100,103 +81,66 @@ public class TrackScheduler {
|
|||
this.repeating = r;
|
||||
}
|
||||
|
||||
public boolean isNightcore() {
|
||||
return filterState.isNightcore();
|
||||
}
|
||||
|
||||
public boolean isVaporwave() {
|
||||
return filterState.isVaporwave();
|
||||
}
|
||||
|
||||
public void setBassboost(int l) {
|
||||
filterState.setBassboost(l);
|
||||
filterState.apply(manager.getLink());
|
||||
filterState.setBassboost(l).apply(manager.getLink());
|
||||
}
|
||||
|
||||
public void setNightcore(boolean e) {
|
||||
filterState.setNightcore(e);
|
||||
filterState.apply(manager.getLink());
|
||||
filterState.setNightcore(e).apply(manager.getLink());
|
||||
}
|
||||
|
||||
public void setVaporwave(boolean e) {
|
||||
filterState.setVaporwave(e);
|
||||
filterState.apply(manager.getLink());
|
||||
filterState.setVaporwave(e).apply(manager.getLink());
|
||||
}
|
||||
|
||||
public void setEqualizerBand(int b, float g) {
|
||||
filterState.setEqualizerBand(b, g);
|
||||
filterState.apply(manager.getLink());
|
||||
filterState.setEqualizerBand(b, g).apply(manager.getLink());
|
||||
}
|
||||
|
||||
public void resetFilters() {
|
||||
filterState.reset();
|
||||
filterState.apply(manager.getLink());
|
||||
}
|
||||
|
||||
public void onTrackStart(Track track) {
|
||||
manager.cancelAutoLeaveTimer();
|
||||
filterState.reset().apply(manager.getLink());
|
||||
}
|
||||
|
||||
public void onTrackEnd(Track lastTrack, AudioTrackEndReason endReason) {
|
||||
if (endReason == AudioTrackEndReason.LOAD_FAILED) {
|
||||
LoggerDecorator.warn(log, "Load failed, skipping to next");
|
||||
Track next = queue.poll();
|
||||
if (next != null) {
|
||||
startTrack(next);
|
||||
} else {
|
||||
manager.startAutoLeaveTimer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!endReason.getMayStartNext() && endReason != AudioTrackEndReason.FINISHED) {
|
||||
nextOrAutoLeave();
|
||||
} else if (!endReason.getMayStartNext() && endReason != AudioTrackEndReason.FINISHED) {
|
||||
LoggerDecorator.warn(log, "Track ended unexpectedly: " + endReason);
|
||||
manager.startAutoLeaveTimer();
|
||||
return;
|
||||
}
|
||||
if (repeating && endReason == AudioTrackEndReason.FINISHED) {
|
||||
} else if (repeating && endReason == AudioTrackEndReason.FINISHED) {
|
||||
startTrack(lastTrack.makeClone());
|
||||
} else {
|
||||
Track next = queue.poll();
|
||||
if (next != null) {
|
||||
startTrack(next);
|
||||
} else {
|
||||
manager.startAutoLeaveTimer();
|
||||
}
|
||||
nextOrAutoLeave();
|
||||
}
|
||||
}
|
||||
|
||||
Queue<Track> getRawQueue() {
|
||||
return queue;
|
||||
}
|
||||
|
||||
void startTrack(Track track) {
|
||||
manager.cancelAutoLeaveTimer();
|
||||
Link link = manager.getLavalinkManager().getOrCreateLink(manager.getGuildId());
|
||||
var link = manager.getLavalinkManager().getOrCreateLink(manager.getGuildId());
|
||||
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) {
|
||||
if (attempt > 5) return;
|
||||
if (link.getCachedPlayer() == null) {
|
||||
manager
|
||||
.getExecutor()
|
||||
.schedule(() -> sendTrackDelayed(link, track, attempt + 1), 300, TimeUnit.MILLISECONDS);
|
||||
scheduleRetry(() -> sendTrackDelayed(link, track, attempt + 1), 300);
|
||||
return;
|
||||
}
|
||||
link.createOrUpdatePlayer()
|
||||
.setTrack(track)
|
||||
.setVolume(100)
|
||||
.subscribe(
|
||||
null,
|
||||
err -> {
|
||||
if (attempt < 5) {
|
||||
manager
|
||||
.getExecutor()
|
||||
.schedule(
|
||||
() -> sendTrackDelayed(link, track, attempt + 1),
|
||||
500,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
});
|
||||
null, err -> scheduleRetry(() -> sendTrackDelayed(link, track, attempt + 1), 500));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,19 @@ import java.util.Set;
|
|||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
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.SearchManager;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import creator.loki.util.VoiceHelper;
|
||||
import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler;
|
||||
import dev.arbjerg.lavalink.client.player.LoadFailed;
|
||||
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
|
||||
import dev.arbjerg.lavalink.client.player.SearchResult;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
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 {
|
||||
private static final Set<String> SOURCES = Set.of("yt", "sc", "list");
|
||||
|
|
@ -48,9 +52,9 @@ public class PlayCommand implements Command {
|
|||
|
||||
@Override
|
||||
public void execute(CommandContext ctx) {
|
||||
var guild = ctx.getGuild();
|
||||
Guild guild = ctx.getGuild();
|
||||
if (guild == null) return;
|
||||
var member = ctx.getAuthor();
|
||||
Member member = ctx.getAuthor();
|
||||
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
|
|
@ -60,11 +64,7 @@ public class PlayCommand implements Command {
|
|||
}
|
||||
var args = ctx.getArgs();
|
||||
if (args.isEmpty()) {
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Аргументы", "Укажи URL, запрос или номер: `" + getUsage() + "`"))
|
||||
.queue();
|
||||
sendErr(ctx, "Укажи URL, запрос или номер: `" + getUsage() + "`");
|
||||
return;
|
||||
}
|
||||
String first = args.get(0);
|
||||
|
|
@ -78,29 +78,16 @@ public class PlayCommand implements Command {
|
|||
return;
|
||||
}
|
||||
var lm = playerManager.getLavalinkManager();
|
||||
if (!guild.getMember(guild.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) {
|
||||
if (!guild.getSelfMember().getVoiceState().inAudioChannel()) {
|
||||
var vc = member.getVoiceState().getChannel().asVoiceChannel();
|
||||
guild.getJDA().getDirectAudioController().connect(vc);
|
||||
if (!guild.getAudioManager().isConnected()) {
|
||||
guild.getAudioManager().openAudioConnection(vc);
|
||||
}
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (guild.getMember(guild.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) break;
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!guild.getAudioManager().isConnected()) guild.getAudioManager().openAudioConnection(vc);
|
||||
var self = guild.getSelfMember();
|
||||
VoiceHelper.waitForCondition(() -> self.getVoiceState().inAudioChannel());
|
||||
if (!self.getVoiceState().inAudioChannel()) return;
|
||||
var link = lm.getOrCreateLink(guild.getIdLong());
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (link.getCachedPlayer() != null) break;
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
VoiceHelper.waitForCondition(() -> link.getCachedPlayer() != null);
|
||||
if (link.getCachedPlayer() == null) return;
|
||||
}
|
||||
var mm = lm.getMusicManager(guild.getIdLong());
|
||||
boolean isSource = SOURCES.contains(first);
|
||||
|
|
@ -109,40 +96,31 @@ public class PlayCommand implements Command {
|
|||
sendErr(ctx, "Укажи запрос после источника.");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isUrl = raw.startsWith("http://") || raw.startsWith("https://");
|
||||
boolean isList = !isUrl && first.equals("list");
|
||||
|
||||
String id;
|
||||
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;
|
||||
String query = isUrl ? null : (first.equals("list") ? raw : raw);
|
||||
String id = buildQuery(first, raw);
|
||||
loadAndHandle(ctx, lm, guild.getIdLong(), mm, id, isUrl, query, raw);
|
||||
}
|
||||
|
||||
private void loadAndHandle(
|
||||
CommandContext ctx,
|
||||
LavalinkManager lm,
|
||||
long guildId,
|
||||
GuildMusicManager mm,
|
||||
String id,
|
||||
boolean isUrl,
|
||||
String query,
|
||||
String raw) {
|
||||
String noMatch =
|
||||
isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw;
|
||||
String query = isList ? raw : (isUrl ? null : raw);
|
||||
|
||||
lm.getOrCreateLink(guild.getIdLong())
|
||||
lm.getOrCreateLink(guildId)
|
||||
.loadItem(id)
|
||||
.subscribe(
|
||||
new AbstractAudioLoadResultHandler() {
|
||||
@Override
|
||||
public void ontrackLoaded(TrackLoaded r) {
|
||||
if (autoPlay) {
|
||||
playTrack(ctx, r.getTrack());
|
||||
} else {
|
||||
showResults(ctx, query, mm, List.of(r.getTrack()));
|
||||
}
|
||||
if (isUrl) playTrack(ctx, r.getTrack());
|
||||
else showResults(ctx, query, mm, List.of(r.getTrack()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -159,7 +137,7 @@ public class PlayCommand implements Command {
|
|||
"Плейлист добавлен",
|
||||
"**" + r.getInfo().getName() + "** — " + c + " треков"))
|
||||
.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));
|
||||
} else {
|
||||
showResults(ctx, query, mm, r.getTracks());
|
||||
|
|
@ -170,7 +148,7 @@ public class PlayCommand implements Command {
|
|||
public void onSearchResultLoaded(SearchResult r) {
|
||||
if (r.getTracks().isEmpty()) {
|
||||
sendErr(ctx, noMatch);
|
||||
} else if (autoPlay) {
|
||||
} else if (isUrl) {
|
||||
playTrack(ctx, r.getTracks().get(0));
|
||||
} else {
|
||||
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) {
|
||||
if (q == null) return;
|
||||
searchHandler.storeResults(ctx.getGuild().getIdLong(), list);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public class FilterSlashHandler {
|
|||
public void handleNightcore(SlashCommandInteractionEvent event) {
|
||||
if (reqVoice(event) == null) return;
|
||||
var manager = getManager(event);
|
||||
boolean now = !manager.getScheduler().isNightcore();
|
||||
boolean now = !manager.getScheduler().filterState.isNightcore();
|
||||
manager.getScheduler().setNightcore(now);
|
||||
event
|
||||
.replyEmbeds(
|
||||
|
|
@ -39,7 +39,7 @@ public class FilterSlashHandler {
|
|||
public void handleVaporwave(SlashCommandInteractionEvent event) {
|
||||
if (reqVoice(event) == null) return;
|
||||
var manager = getManager(event);
|
||||
boolean now = !manager.getScheduler().isVaporwave();
|
||||
boolean now = !manager.getScheduler().filterState.isVaporwave();
|
||||
manager.getScheduler().setVaporwave(now);
|
||||
event
|
||||
.replyEmbeds(
|
||||
|
|
|
|||
|
|
@ -2,193 +2,144 @@ package creator.loki.feature.music.slash;
|
|||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class QueueSlashHandler {
|
||||
private final PlayerManager playerManager;
|
||||
private final PlayerManager pm;
|
||||
|
||||
public QueueSlashHandler(PlayerManager playerManager) {
|
||||
this.playerManager = playerManager;
|
||||
public QueueSlashHandler(PlayerManager pm) {
|
||||
this.pm = pm;
|
||||
}
|
||||
|
||||
public void handleStop(SlashCommandInteractionEvent event) {
|
||||
if (reqVoice(event) == null) return;
|
||||
var manager = getManager(event);
|
||||
manager.stop();
|
||||
event.getGuild().getJDA().getDirectAudioController().disconnect(event.getGuild());
|
||||
event
|
||||
.replyEmbeds(EmbedTemplates.info("Остановлено", "Музыка остановлена, очередь очищена."))
|
||||
public void handleStop(SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) return;
|
||||
pm.getMusicManager(ev.getGuild().getIdLong()).stop();
|
||||
ev.getGuild().getJDA().getDirectAudioController().disconnect(ev.getGuild());
|
||||
ev.replyEmbeds(EmbedTemplates.info("Остановлено", "Музыка остановлена, очередь очищена."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
public void handleSkip(SlashCommandInteractionEvent event) {
|
||||
if (reqVoice(event) == null) return;
|
||||
var manager = getManager(event);
|
||||
if (manager.getScheduler().getCurrentTrack() == null) {
|
||||
event.replyEmbeds(EmbedTemplates.error("Очередь пуста", "Сейчас ничего не играет.")).queue();
|
||||
public void handleSkip(SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) return;
|
||||
var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
|
||||
if (s.getCurrentTrack() == null) {
|
||||
ev.replyEmbeds(EmbedTemplates.error("Очередь пуста", "Сейчас ничего не играет.")).queue();
|
||||
return;
|
||||
}
|
||||
String skipped = manager.getScheduler().getCurrentTrack().getInfo().getTitle();
|
||||
manager.getScheduler().skip();
|
||||
event.replyEmbeds(EmbedTemplates.info("Пропущено", "**" + skipped + "**")).queue();
|
||||
var t = s.getCurrentTrack().getInfo().getTitle();
|
||||
s.skip();
|
||||
ev.replyEmbeds(EmbedTemplates.info("Пропущено", "**" + t + "**")).queue();
|
||||
}
|
||||
|
||||
public void handleQueue(SlashCommandInteractionEvent event) {
|
||||
var manager = getManager(event);
|
||||
Track current = manager.getScheduler().getCurrentTrack();
|
||||
List<Track> queue = manager.getScheduler().getQueue();
|
||||
if (current == null && queue.isEmpty()) {
|
||||
event.reply("Очередь пуста.").queue();
|
||||
public void handleQueue(SlashCommandInteractionEvent ev) {
|
||||
var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
|
||||
var cur = s.getCurrentTrack();
|
||||
var q = s.getQueue();
|
||||
if (cur == null && q.isEmpty()) {
|
||||
ev.reply("Очередь пуста.").queue();
|
||||
return;
|
||||
}
|
||||
var embed =
|
||||
EmbedFactory.create().withTitle("Очередь треков").withColor(new Color(88, 101, 242));
|
||||
if (current != null) {
|
||||
embed.addField(
|
||||
"Сейчас играет",
|
||||
"**" + current.getInfo().getTitle() + "**\n" + current.getInfo().getAuthor(),
|
||||
false);
|
||||
var eb = EmbedFactory.create().withTitle("Очередь треков").withColor(new Color(88, 101, 242));
|
||||
if (cur != null) {
|
||||
var ci = cur.getInfo();
|
||||
eb.addField("Сейчас играет", "**" + ci.getTitle() + "**\n" + ci.getAuthor(), false);
|
||||
}
|
||||
if (!queue.isEmpty()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
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();
|
||||
if (!q.isEmpty()) eb.addField("В очереди (" + q.size() + ")", buildQueueList(q), false);
|
||||
ev.replyEmbeds(eb.build()).queue();
|
||||
}
|
||||
|
||||
public void handleNowPlaying(SlashCommandInteractionEvent event) {
|
||||
var manager = getManager(event);
|
||||
var track = manager.getScheduler().getCurrentTrack();
|
||||
private String buildQueueList(List<Track> q) {
|
||||
var sb = new StringBuilder();
|
||||
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) {
|
||||
event.reply("Сейчас ничего не играет.").queue();
|
||||
ev.reply("Сейчас ничего не играет.").queue();
|
||||
return;
|
||||
}
|
||||
var link =
|
||||
playerManager
|
||||
.getLavalinkManager()
|
||||
.getClient()
|
||||
.getLinkIfCached(event.getGuild().getIdLong());
|
||||
long pos =
|
||||
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getPosition() : 0;
|
||||
long dur = track.getInfo().getLength();
|
||||
event
|
||||
.replyEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("Сейчас играет")
|
||||
.withColor(new Color(88, 101, 242))
|
||||
.addField("Название", "**" + track.getInfo().getTitle() + "**", false)
|
||||
.addField("Автор", track.getInfo().getAuthor(), true)
|
||||
.addField("Длительность", progressBar(pos, dur), true)
|
||||
.build())
|
||||
.queue();
|
||||
var link = pm.getLavalinkManager().getClient().getLinkIfCached(ev.getGuild().getIdLong());
|
||||
var pos =
|
||||
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getPosition() : 0L;
|
||||
var eb =
|
||||
EmbedFactory.create()
|
||||
.withTitle("Сейчас играет")
|
||||
.withColor(new Color(88, 101, 242))
|
||||
.addField("Название", "**" + track.getInfo().getTitle() + "**", false)
|
||||
.addField("Автор", track.getInfo().getAuthor(), true)
|
||||
.addField("Длительность", progressBar(pos, track.getInfo().getLength()), true)
|
||||
.build();
|
||||
ev.replyEmbeds(eb).queue();
|
||||
}
|
||||
|
||||
public void handleVolume(SlashCommandInteractionEvent event) {
|
||||
if (reqVoice(event) == null) return;
|
||||
var link =
|
||||
playerManager
|
||||
.getLavalinkManager()
|
||||
.getClient()
|
||||
.getLinkIfCached(event.getGuild().getIdLong());
|
||||
Integer percent =
|
||||
event.getOption("percent") != null ? event.getOption("percent").getAsInt() : null;
|
||||
if (percent == null) {
|
||||
int vol =
|
||||
public void handleVolume(SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) return;
|
||||
var link = pm.getLavalinkManager().getClient().getLinkIfCached(ev.getGuild().getIdLong());
|
||||
var opt = ev.getOption("percent");
|
||||
if (opt == null) {
|
||||
var cur =
|
||||
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getVolume() : 100;
|
||||
event.replyEmbeds(EmbedTemplates.info("Громкость", "Текущая: **" + vol + "%**")).queue();
|
||||
ev.replyEmbeds(EmbedTemplates.info("Громкость", "Текущая: **" + cur + "%**")).queue();
|
||||
return;
|
||||
}
|
||||
int vol = Math.max(0, Math.min(100, percent));
|
||||
if (link != null && link.getCachedPlayer() != null) {
|
||||
link.createOrUpdatePlayer()
|
||||
.setVolume(vol)
|
||||
.subscribe(
|
||||
null,
|
||||
err ->
|
||||
LoggerFactory.getLogger(QueueSlashHandler.class)
|
||||
.warn("Lavalink: {}", err.getMessage()));
|
||||
}
|
||||
event.replyEmbeds(EmbedTemplates.info("Громкость", "Установлена на **" + vol + "%**")).queue();
|
||||
var vol = Math.max(0, Math.min(100, opt.getAsInt()));
|
||||
if (link != null && link.getCachedPlayer() != null)
|
||||
link.createOrUpdatePlayer().setVolume(vol).subscribe(null, LOG);
|
||||
ev.replyEmbeds(EmbedTemplates.info("Громкость", "Установлена на **" + vol + "%**")).queue();
|
||||
}
|
||||
|
||||
public void handlePause(SlashCommandInteractionEvent event) {
|
||||
if (reqVoice(event) == null) return;
|
||||
getManager(event)
|
||||
public void handlePause(SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) return;
|
||||
pm.getMusicManager(ev.getGuild().getIdLong())
|
||||
.getPlayer()
|
||||
.ifPresent(
|
||||
p ->
|
||||
p.setPaused(true)
|
||||
.subscribe(
|
||||
null,
|
||||
err ->
|
||||
LoggerFactory.getLogger(QueueSlashHandler.class)
|
||||
.warn("Lavalink: {}", err.getMessage())));
|
||||
event.replyEmbeds(EmbedTemplates.info("Пауза", "Музыка поставлена на паузу.")).queue();
|
||||
.ifPresent(pl -> pl.setPaused(true).subscribe(null, LOG));
|
||||
ev.replyEmbeds(EmbedTemplates.info("Пауза", "Музыка поставлена на паузу.")).queue();
|
||||
}
|
||||
|
||||
public void handleResume(SlashCommandInteractionEvent event) {
|
||||
if (reqVoice(event) == null) return;
|
||||
getManager(event)
|
||||
public void handleResume(SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) return;
|
||||
pm.getMusicManager(ev.getGuild().getIdLong())
|
||||
.getPlayer()
|
||||
.ifPresent(
|
||||
p ->
|
||||
p.setPaused(false)
|
||||
.subscribe(
|
||||
null,
|
||||
err ->
|
||||
LoggerFactory.getLogger(QueueSlashHandler.class)
|
||||
.warn("Lavalink: {}", err.getMessage())));
|
||||
event.replyEmbeds(EmbedTemplates.info("Продолжаем", "Воспроизведение продолжено.")).queue();
|
||||
.ifPresent(pl -> pl.setPaused(false).subscribe(null, LOG));
|
||||
ev.replyEmbeds(EmbedTemplates.info("Продолжаем", "Воспроизведение продолжено.")).queue();
|
||||
}
|
||||
|
||||
public void handleShuffle(SlashCommandInteractionEvent event) {
|
||||
if (reqVoice(event) == null) return;
|
||||
getManager(event).getScheduler().shuffle();
|
||||
event.replyEmbeds(EmbedTemplates.info("Перемешано", "Очередь перемешана.")).queue();
|
||||
public void handleShuffle(SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) return;
|
||||
pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler().shuffle();
|
||||
ev.replyEmbeds(EmbedTemplates.info("Перемешано", "Очередь перемешана.")).queue();
|
||||
}
|
||||
|
||||
public void handleRepeat(SlashCommandInteractionEvent event) {
|
||||
if (reqVoice(event) == null) return;
|
||||
var s = getManager(event).getScheduler();
|
||||
boolean now = !s.isRepeating();
|
||||
public void handleRepeat(SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) return;
|
||||
var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
|
||||
var now = !s.isRepeating();
|
||||
s.setRepeating(now);
|
||||
event
|
||||
.replyEmbeds(
|
||||
EmbedTemplates.info(
|
||||
"Повтор", now ? "Повтор очереди включён." : "Повтор очереди выключен."))
|
||||
.queue();
|
||||
var d = now ? "Повтор очереди включён." : "Повтор очереди выключен.";
|
||||
ev.replyEmbeds(EmbedTemplates.info("Повтор", d)).queue();
|
||||
}
|
||||
|
||||
private Member reqVoice(SlashCommandInteractionEvent event) {
|
||||
Member member = event.getMember();
|
||||
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
||||
event
|
||||
.replyEmbeds(EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала."))
|
||||
.setEphemeral(true)
|
||||
.queue();
|
||||
return null;
|
||||
}
|
||||
return member;
|
||||
}
|
||||
|
||||
private GuildMusicManager getManager(SlashCommandInteractionEvent event) {
|
||||
return playerManager.getMusicManager(event.getGuild().getIdLong());
|
||||
private Member reqVoice(SlashCommandInteractionEvent ev) {
|
||||
var m = ev.getMember();
|
||||
if (m != null && m.getVoiceState().inAudioChannel()) return m;
|
||||
var em = EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала.");
|
||||
ev.replyEmbeds(em).setEphemeral(true).queue();
|
||||
return null;
|
||||
}
|
||||
|
||||
static String progressBar(long pos, long dur) {
|
||||
|
|
@ -197,11 +148,12 @@ public class QueueSlashHandler {
|
|||
int filled = (int) ((double) pos / dur * total);
|
||||
var bar = new StringBuilder("\u2503");
|
||||
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) {
|
||||
long s = millis / 1000;
|
||||
return String.format("%d:%02d", s / 60, s % 60);
|
||||
}
|
||||
private static final Consumer<Throwable> LOG =
|
||||
err ->
|
||||
LoggerFactory.getLogger(QueueSlashHandler.class).warn("Lavalink: {}", err.getMessage());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -1,158 +1,140 @@
|
|||
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.FontMetrics;
|
||||
import java.awt.GradientPaint;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
public class RankCardGenerator {
|
||||
private static final int WIDTH = 1400;
|
||||
private static final int HEIGHT = 500;
|
||||
private static final Map<String, Font> FONT_CACHE = new ConcurrentHashMap<>();
|
||||
private final RankCardRenderer renderer = new RankCardRenderer();
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
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();
|
||||
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(
|
||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
|
||||
BufferedImage bg = loadBackground("backgrounds/" + data.getBackground());
|
||||
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);
|
||||
RankCardRenderer.applyHints(g);
|
||||
g.drawImage(renderer.loadBackground("backgrounds/" + data.getBackground()), 0, 0, W, H, null);
|
||||
g.setColor(OVERLAY);
|
||||
g.fillRect(0, 0, W, H);
|
||||
drawCard(g, PAD, PAD, W - PAD * 2, H - PAD * 2);
|
||||
drawRankBadge(g, data.getRank());
|
||||
|
||||
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));
|
||||
g.setColor(new Color(255, 140, 0));
|
||||
g.drawOval(70, 90, 180, 180);
|
||||
int nameY = AVATAR_Y + 58;
|
||||
g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 50f));
|
||||
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.setColor(Color.WHITE);
|
||||
g.drawString(data.getUsername(), 320, 150);
|
||||
g.setFont(renderer.loadFont("fonts/Inter-Regular.ttf", 26f));
|
||||
g.setColor(GREY);
|
||||
g.drawString(data.getSubtitle(), CONTENT_X, nameY + 36);
|
||||
|
||||
g.setFont(loadFont("fonts/Inter-Bold.ttf", 24f));
|
||||
g.setColor(new Color(255, 140, 0));
|
||||
g.fillRoundRect(670, 110, 90, 42, 15, 15);
|
||||
g.setColor(Color.BLACK);
|
||||
g.drawString(data.getTag(), 690, 138);
|
||||
int barLabelY = nameY + 100;
|
||||
g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 22f));
|
||||
g.setColor(GREY);
|
||||
g.drawString("ОПЫТ", CONTENT_X, barLabelY);
|
||||
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));
|
||||
g.setColor(new Color(220, 220, 220));
|
||||
g.drawString(data.getSubtitle(), 320, 210);
|
||||
|
||||
g.setFont(loadFont("fonts/Inter-Bold.ttf", 48f));
|
||||
g.setColor(new Color(255, 140, 0));
|
||||
g.drawString(String.valueOf(data.getLevel()), 320, 300);
|
||||
g.setFont(loadFont("fonts/Inter-Bold.ttf", 36f));
|
||||
g.drawString("УРОВЕНЬ", 380, 300);
|
||||
|
||||
g.setFont(loadFont("fonts/Inter-Regular.ttf", 34f));
|
||||
g.setColor(Color.WHITE);
|
||||
g.drawString(data.getXp() + " / " + data.getMaxXp() + " XP", 920, 300);
|
||||
|
||||
int barX = 320;
|
||||
int barY = 340;
|
||||
int barWidth = 620;
|
||||
int barHeight = 28;
|
||||
g.setColor(new Color(255, 255, 255, 40));
|
||||
g.fillRoundRect(barX, barY, barWidth, barHeight, 20, 20);
|
||||
|
||||
float progress = (float) data.getXp() / data.getMaxXp();
|
||||
int fill = (int) (barWidth * progress);
|
||||
g.setColor(new Color(255, 140, 0));
|
||||
g.fillRoundRect(barX, barY, fill, barHeight, 20, 20);
|
||||
|
||||
int dotX = barX + fill - 10;
|
||||
g.setColor(Color.WHITE);
|
||||
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);
|
||||
int statW = (CONTENT_W - STAT_GAP * 2) / 3;
|
||||
renderer.drawStatCard(
|
||||
g,
|
||||
"АКТИВНОСТЬ",
|
||||
data.getActivity(),
|
||||
RankCardIcons.StatIcon.CLOCK,
|
||||
CONTENT_X,
|
||||
STATS_Y,
|
||||
statW,
|
||||
STAT_H);
|
||||
renderer.drawStatCard(
|
||||
g,
|
||||
"СООБЩЕНИЙ",
|
||||
String.valueOf(data.getMessages()),
|
||||
RankCardIcons.StatIcon.MESSAGE,
|
||||
CONTENT_X + statW + STAT_GAP,
|
||||
STATS_Y,
|
||||
statW,
|
||||
STAT_H);
|
||||
renderer.drawStatCard(
|
||||
g,
|
||||
"РЕПУТАЦИЯ",
|
||||
String.valueOf(data.getReputation()),
|
||||
RankCardIcons.StatIcon.STAR,
|
||||
CONTENT_X + (statW + STAT_GAP) * 2,
|
||||
STATS_Y,
|
||||
statW,
|
||||
STAT_H);
|
||||
|
||||
g.dispose();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
private enum IconType {
|
||||
CLOCK,
|
||||
MESSAGE,
|
||||
STAR
|
||||
private void drawCard(Graphics2D g, int x, int y, int w, int h) {
|
||||
RoundRectangle2D rect = new RoundRectangle2D.Float(x, y, w, h, 36, 36);
|
||||
g.setColor(new Color(10, 10, 18, 210));
|
||||
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) {
|
||||
g.setColor(new Color(15, 15, 15, 180));
|
||||
g.fillRoundRect(x, y, 340, 110, 30, 30);
|
||||
|
||||
g.setColor(new Color(255, 140, 0));
|
||||
g.fillOval(x + 25, y + 25, 48, 48);
|
||||
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);
|
||||
private void drawAvatar(Graphics2D g, BufferedImage src, int x, int y, int size) {
|
||||
g.setColor(new Color(255, 145, 0, 45));
|
||||
g.fillOval(x - 10, y - 10, size + 20, size + 20);
|
||||
Ellipse2D clip = new Ellipse2D.Double(x, y, size, size);
|
||||
g.setClip(clip);
|
||||
g.drawImage(src, x, y, size, size, 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) {
|
||||
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("Arial", Font.PLAIN, (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("Arial", Font.PLAIN, (int) size);
|
||||
FONT_CACHE.put(key, fallback);
|
||||
return fallback;
|
||||
}
|
||||
private void drawLevelTag(Graphics2D g, int level, int x, int y) {
|
||||
String text = "LVL " + level;
|
||||
g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 20f));
|
||||
FontMetrics fm = g.getFontMetrics();
|
||||
int tw = fm.stringWidth(text);
|
||||
int th = fm.getAscent();
|
||||
int px = 14, py = 8;
|
||||
g.setColor(ACCENT);
|
||||
g.fillRoundRect(x, y, tw + px * 2, th + py * 2, 12, 12);
|
||||
g.setColor(new Color(20, 10, 0));
|
||||
g.drawString(text, x + px, y + py + th - 2);
|
||||
}
|
||||
|
||||
private BufferedImage loadBackground(String path) throws Exception {
|
||||
try (InputStream is = getClass().getClassLoader().getResourceAsStream("assets/" + path)) {
|
||||
if (is == null) {
|
||||
throw new IllegalArgumentException("Resource not found: assets/" + path);
|
||||
}
|
||||
return ImageIO.read(is);
|
||||
}
|
||||
private void drawRankBadge(Graphics2D g, int rank) {
|
||||
int bx = W - PAD - 20 - 110;
|
||||
int by = PAD + 22;
|
||||
g.setColor(new Color(20, 20, 30, 200));
|
||||
g.fillRoundRect(bx, by, 110, 44, 14, 14);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
}
|
||||
118
src/main/java/creator/loki/feature/profile/RankCardRenderer.java
Normal file
118
src/main/java/creator/loki/feature/profile/RankCardRenderer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,15 @@ package creator.loki.feature.radio;
|
|||
|
||||
import java.util.Map;
|
||||
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.manage.GuildMusicManager;
|
||||
import creator.loki.feature.music.repository.QueueRepository;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import creator.loki.util.VoiceHelper;
|
||||
import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler;
|
||||
import dev.arbjerg.lavalink.client.Link;
|
||||
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.LoggerFactory;
|
||||
|
||||
@Singleton
|
||||
public class RadioManager {
|
||||
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 QueueRepository queueRepository;
|
||||
private LavalinkManager lavalinkManager;
|
||||
private JDA radioJda;
|
||||
|
||||
public void setLavalinkManager(LavalinkManager lm) {
|
||||
this.lavalinkManager = lm;
|
||||
@Inject
|
||||
public RadioManager(QueueRepository queueRepository) {
|
||||
this.queueRepository = queueRepository;
|
||||
}
|
||||
|
||||
public void setRadioJda(JDA radioJda) {
|
||||
this.radioJda = radioJda;
|
||||
}
|
||||
public void setLavalinkManager(LavalinkManager lm) { this.lavalinkManager = lm; }
|
||||
|
||||
public void setRadioJda(JDA radioJda) { this.radioJda = radioJda; }
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -43,86 +56,65 @@ public class RadioManager {
|
|||
GuildRadioState state = radioStates.get(guildId);
|
||||
if (state == null || state.streamUrl == null || state.streamUrl.isBlank()) 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);
|
||||
if (guild == null) return;
|
||||
|
||||
guild.getJDA().getDirectAudioController().connect(
|
||||
guild.getVoiceChannelById(channelId));
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (guild.getMember(guild.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) break;
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
guild.getJDA().getDirectAudioController().connect(guild.getVoiceChannelById(channelId));
|
||||
var self = guild.getMember(guild.getJDA().getSelfUser());
|
||||
if (!VoiceHelper.waitForCondition(() -> self.getVoiceState().inAudioChannel(), VOICE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
|
||||
LoggerDecorator.warn(logger, "Radio: voice connection timeout for guild " + guildId);
|
||||
return;
|
||||
}
|
||||
|
||||
var link = lavalinkManager.getOrCreateLink(guildId);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (link.getCachedPlayer() != null) break;
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
if (!VoiceHelper.waitForCondition(() -> link.getCachedPlayer() != null, VOICE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
|
||||
LoggerDecorator.warn(logger, "Radio: Lavalink player timeout for guild " + guildId);
|
||||
return;
|
||||
}
|
||||
|
||||
var mm = new GuildMusicManager(guildId, lavalinkManager, null);
|
||||
state.musicManager = mm;
|
||||
|
||||
lavalinkManager
|
||||
.getOrCreateLink(guildId)
|
||||
.loadItem(state.streamUrl)
|
||||
.subscribe(
|
||||
new AbstractAudioLoadResultHandler() {
|
||||
@Override
|
||||
public void ontrackLoaded(TrackLoaded r) {
|
||||
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()) {
|
||||
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()) {
|
||||
mm.getScheduler().enqueue(r.getTracks().get(0));
|
||||
LoggerDecorator.music(logger, "Radio started in guild " + guildId);
|
||||
}
|
||||
}
|
||||
|
||||
@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"));
|
||||
}
|
||||
});
|
||||
var mm = new GuildMusicManager(guildId, lavalinkManager, queueRepository);
|
||||
int vol = finalState.volume;
|
||||
var url = finalState.streamUrl;
|
||||
finalState.musicManager = mm;
|
||||
lavalinkManager.getOrCreateLink(guildId).loadItem(url)
|
||||
.subscribe(new AbstractAudioLoadResultHandler() {
|
||||
@Override
|
||||
public void ontrackLoaded(TrackLoaded r) {
|
||||
if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe();
|
||||
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()) {
|
||||
if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe();
|
||||
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();
|
||||
mm.getScheduler().enqueue(r.getTracks().get(0));
|
||||
LoggerDecorator.music(logger, "Radio started in guild " + guildId);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
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"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stopRadio(long guildId) {
|
||||
GuildRadioState state = radioStates.remove(guildId);
|
||||
if (state == null) return;
|
||||
if (state.musicManager != null) {
|
||||
state.musicManager.stop();
|
||||
state.musicManager.destroy();
|
||||
}
|
||||
if (state.musicManager != null) { state.musicManager.stop(); state.musicManager.destroy(); }
|
||||
if (lavalinkManager != null) {
|
||||
Link link = lavalinkManager.getClient().getLinkIfCached(guildId);
|
||||
if (link != null) link.destroy().subscribe();
|
||||
|
|
@ -135,16 +127,13 @@ public class RadioManager {
|
|||
}
|
||||
|
||||
public boolean isRadioActive(long guildId) {
|
||||
return radioStates.containsKey(guildId);
|
||||
GuildRadioState state = radioStates.get(guildId);
|
||||
return state != null && state.streamUrl != null;
|
||||
}
|
||||
|
||||
public boolean hasRadioBot() {
|
||||
return radioJda != null;
|
||||
}
|
||||
public boolean hasRadioBot() { return radioJda != null; }
|
||||
|
||||
public JDA getRadioJda() {
|
||||
return radioJda;
|
||||
}
|
||||
public JDA getRadioJda() { return radioJda; }
|
||||
|
||||
public int getVolume(long guildId) {
|
||||
GuildRadioState state = radioStates.get(guildId);
|
||||
|
|
@ -152,13 +141,13 @@ public class RadioManager {
|
|||
}
|
||||
|
||||
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));
|
||||
if (lavalinkManager != null) {
|
||||
var link = lavalinkManager.getClient().getLinkIfCached(guildId);
|
||||
if (link != null && link.getCachedPlayer() != null) {
|
||||
link.createOrUpdatePlayer()
|
||||
.setVolume(state.volume)
|
||||
link.createOrUpdatePlayer().setVolume(state.volume)
|
||||
.subscribe(null, err -> logger.warn("Lavalink: {}", err.getMessage()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,15 +8,16 @@ import java.awt.image.BufferedImage;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import creator.loki.feature.counter.DailyStats;
|
||||
import creator.loki.feature.counter.repository.CounterStatsRepository;
|
||||
import net.dv8tion.jda.api.OnlineStatus;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
|
||||
public class ServerStatsService {
|
||||
|
||||
private static final int CHART_WIDTH = 800;
|
||||
private static final int CHART_HEIGHT = 400;
|
||||
private static final int MARGIN = 60;
|
||||
|
|
@ -38,62 +39,56 @@ public class ServerStatsService {
|
|||
}
|
||||
|
||||
public byte[] generateChart(List<DailyStats> history) throws Exception {
|
||||
BufferedImage image = new BufferedImage(CHART_WIDTH, CHART_HEIGHT, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = image.createGraphics();
|
||||
|
||||
var image = new BufferedImage(CHART_WIDTH, CHART_HEIGHT, BufferedImage.TYPE_INT_RGB);
|
||||
var g = image.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(
|
||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
|
||||
g.setColor(COLOR_BG);
|
||||
g.fillRect(0, 0, CHART_WIDTH, CHART_HEIGHT);
|
||||
|
||||
int maxVal =
|
||||
var maxVal =
|
||||
history.stream().mapToInt(s -> Math.max(s.getJoined(), s.getLeft())).max().orElse(1);
|
||||
|
||||
int plotW = CHART_WIDTH - 2 * MARGIN;
|
||||
int plotH = CHART_HEIGHT - 2 * MARGIN;
|
||||
int barW = Math.max(4, plotW / (history.size() * 2 + 1));
|
||||
|
||||
var plotW = CHART_WIDTH - 2 * MARGIN;
|
||||
var plotH = CHART_HEIGHT - 2 * MARGIN;
|
||||
var barW = Math.max(4, plotW / (history.size() * 2 + 1));
|
||||
g.setColor(COLOR_GRID);
|
||||
for (int i = 0; i <= 4; i++) {
|
||||
int y = MARGIN + plotH - (plotH * i / 4);
|
||||
for (var i = 0; i <= 4; i++) {
|
||||
var y = MARGIN + plotH - (plotH * i / 4);
|
||||
g.drawLine(MARGIN, y, CHART_WIDTH - MARGIN, y);
|
||||
}
|
||||
|
||||
Font labelFont = new Font("Arial", Font.PLAIN, 11);
|
||||
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);
|
||||
drawLabels(g, plotH, maxVal);
|
||||
var maxBars = Math.min(history.size(), 30);
|
||||
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);
|
||||
|
||||
for (int i = 0; i < history.size(); i++) {
|
||||
DailyStats stats = history.get(i);
|
||||
int x = MARGIN + i * (barW * 2 + 2);
|
||||
|
||||
int joinedH = (int) ((double) stats.getJoined() / maxVal * plotH);
|
||||
int leftH = (int) ((double) stats.getLeft() / maxVal * plotH);
|
||||
private void drawLabels(Graphics2D g, int plotH, int maxVal) {
|
||||
g.setColor(COLOR_TEXT);
|
||||
g.setFont(new Font("Arial", Font.BOLD, 16));
|
||||
g.drawString("Активность сервера (присоединились / вышли)", 160, 30);
|
||||
g.setFont(new Font("Arial", Font.PLAIN, 11));
|
||||
for (var i = 0; i <= 4; i++) {
|
||||
var y = MARGIN + plotH - (plotH * i / 4);
|
||||
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.fillRect(x, MARGIN + plotH - joinedH, barW, joinedH);
|
||||
|
||||
g.setColor(COLOR_LEFT);
|
||||
g.fillRect(x + barW + 2, MARGIN + plotH - leftH, barW, leftH);
|
||||
|
||||
if (i % step == 0) {
|
||||
g.setColor(COLOR_TEXT);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawLegend(Graphics2D g) {
|
||||
g.setFont(new Font("Arial", Font.PLAIN, 12));
|
||||
g.setColor(COLOR_JOINED);
|
||||
g.fillRect(CHART_WIDTH - 170, 10, 12, 12);
|
||||
g.setColor(COLOR_TEXT);
|
||||
g.drawString("Присоединились", CHART_WIDTH - 152, 21);
|
||||
|
||||
g.setColor(COLOR_LEFT);
|
||||
g.fillRect(CHART_WIDTH - 60, 10, 12, 12);
|
||||
g.setColor(COLOR_TEXT);
|
||||
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) {
|
||||
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";
|
||||
String info =
|
||||
String.format(
|
||||
"%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);
|
||||
return (header + info + "\ndate,joined,left,difference\n" + dailyRows + totals)
|
||||
.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String escapeCsv(String value) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,13 @@ import com.google.inject.Inject;
|
|||
import com.google.inject.Singleton;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
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.channel.concrete.VoiceChannel;
|
||||
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
|
|
|||
|
|
@ -9,19 +9,22 @@ import creator.loki.feature.voice.repository.VoicePresetRepository;
|
|||
import creator.loki.util.LoggerDecorator;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
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.Member;
|
||||
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.LoggerFactory;
|
||||
|
||||
public class VoiceChannelManager {
|
||||
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 VoicePresetRepository presetRepo;
|
||||
|
||||
private String triggerChannelId;
|
||||
private String categoryId;
|
||||
|
||||
|
|
@ -41,56 +44,36 @@ public class VoiceChannelManager {
|
|||
|
||||
public VoiceChannel createPersonalChannel(Member member, Guild guild) {
|
||||
try {
|
||||
String channelName = "Канал: " + member.getEffectiveName();
|
||||
|
||||
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()));
|
||||
|
||||
var channel = buildPersonalChannel(member, guild);
|
||||
channelRepo.saveChannelData(new VoiceChannelData(channel.getId(), member.getId()));
|
||||
LoggerDecorator.voice(
|
||||
logger,
|
||||
"Создан личный канал "
|
||||
+ voiceChannel.getId()
|
||||
+ " для пользователя "
|
||||
+ member.getEffectiveName());
|
||||
|
||||
applyUserPreset(voiceChannel, member.getId());
|
||||
|
||||
return voiceChannel;
|
||||
"Создан личный канал %s для пользователя %s"
|
||||
.formatted(channel.getId(), member.getEffectiveName()));
|
||||
applyUserPreset(channel, member.getId());
|
||||
return channel;
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.error(logger, "Ошибка при создании личного канала", e);
|
||||
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) {
|
||||
channelRepo.deleteChannelData(channelId);
|
||||
try {
|
||||
VoiceChannel channel = guild.getVoiceChannelById(channelId);
|
||||
var channel = guild.getVoiceChannelById(channelId);
|
||||
if (channel != null) {
|
||||
channel
|
||||
.delete()
|
||||
.queue(
|
||||
success -> {
|
||||
channelRepo.deleteChannelData(channelId);
|
||||
LoggerDecorator.voice(logger, "Удален личный канал " + channelId);
|
||||
},
|
||||
error ->
|
||||
LoggerDecorator.error(
|
||||
logger, "Ошибка при удалении канала " + channelId, error));
|
||||
} else {
|
||||
channelRepo.deleteChannelData(channelId);
|
||||
.queue(success -> LoggerDecorator.voice(logger, "Удален личный канал " + channelId));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.error(logger, "Ошибка при удалении личного канала", e);
|
||||
|
|
@ -110,43 +93,32 @@ public class VoiceChannelManager {
|
|||
}
|
||||
|
||||
public void updatePresetSetting(String userId, String setting, Object value) {
|
||||
VoicePreset preset = getUserPreset(userId);
|
||||
switch (setting) {
|
||||
case "name" -> preset.setName((String) value);
|
||||
case "limit" -> preset.setLimit((Integer) value);
|
||||
case "bitrate" -> preset.setBitrate((Integer) value);
|
||||
case "locked" -> preset.setLocked((Boolean) value);
|
||||
default -> {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
var preset = getUserPreset(userId);
|
||||
if ("name".equals(setting)) preset.setName((String) value);
|
||||
else if ("limit".equals(setting)) preset.setLimit((Integer) value);
|
||||
else if ("bitrate".equals(setting)) preset.setBitrate((Integer) value);
|
||||
else if ("locked".equals(setting)) preset.setLocked((Boolean) value);
|
||||
saveUserPreset(userId, preset);
|
||||
}
|
||||
|
||||
public void sendControlPanel(VoiceChannel channel, Member owner) {
|
||||
EmbedBuilder embed =
|
||||
var limitText = channel.getUserLimit() == 0 ? "∞" : String.valueOf(channel.getUserLimit());
|
||||
var embed =
|
||||
new EmbedBuilder()
|
||||
.setTitle("🎛️ Управление каналом")
|
||||
.setDescription("Ваш личный голосовой канал")
|
||||
.addField("Владелец", owner.getAsMention(), true)
|
||||
.addField(
|
||||
"Лимит",
|
||||
channel.getUserLimit() == 0 ? "∞" : String.valueOf(channel.getUserLimit()),
|
||||
true)
|
||||
.addField("Лимит", limitText, true)
|
||||
.setColor(Color.CYAN)
|
||||
.setFooter("Управляйте каналом с помощью кнопок ниже");
|
||||
|
||||
String cid = channel.getId();
|
||||
Button lockBtn = Button.primary("voice_lock:" + cid, "🔒 Замок");
|
||||
Button limitBtn = Button.secondary("voice_limit:" + cid, "👥 Лимит");
|
||||
Button renameBtn = Button.secondary("voice_rename:" + cid, "✏️ Название");
|
||||
Button bitrateBtn = Button.secondary("voice_bitrate:" + cid, "🎵 Битрейт");
|
||||
Button deleteBtn = Button.danger("voice_delete:" + cid, "🗑️ Удалить");
|
||||
|
||||
channel
|
||||
.sendMessageEmbeds(embed.build())
|
||||
.addComponents(ActionRow.of(lockBtn, limitBtn, renameBtn, bitrateBtn, deleteBtn))
|
||||
.queue();
|
||||
var row =
|
||||
ActionRow.of(
|
||||
Button.primary("voice_lock:" + channel.getId(), "🔒 Замок"),
|
||||
Button.secondary("voice_limit:" + channel.getId(), "👥 Лимит"),
|
||||
Button.secondary("voice_rename:" + channel.getId(), "✏️ Название"),
|
||||
Button.secondary("voice_bitrate:" + channel.getId(), "🎵 Битрейт"),
|
||||
Button.danger("voice_delete:" + channel.getId(), "🗑️ Удалить"));
|
||||
channel.sendMessageEmbeds(embed.build()).addComponents(row).queue();
|
||||
}
|
||||
|
||||
public void saveUserPreset(String userId, VoicePreset preset) {
|
||||
|
|
@ -155,28 +127,19 @@ public class VoiceChannelManager {
|
|||
}
|
||||
|
||||
public VoicePreset getUserPreset(String userId) {
|
||||
VoicePreset preset = presetRepo.getPreset(userId);
|
||||
return preset != null ? preset : new VoicePreset();
|
||||
return presetRepo.getPreset(userId) != null ? presetRepo.getPreset(userId) : new VoicePreset();
|
||||
}
|
||||
|
||||
public void applyUserPreset(VoiceChannel channel, String userId) {
|
||||
VoicePreset preset = getUserPreset(userId);
|
||||
|
||||
var preset = getUserPreset(userId);
|
||||
try {
|
||||
if (preset.getName() != null && !preset.getName().isEmpty()) {
|
||||
if (preset.getName() != null && !preset.getName().isEmpty())
|
||||
channel.getManager().setName(preset.getName()).queue();
|
||||
}
|
||||
if (preset.getLimit() >= 0) {
|
||||
channel.getManager().setUserLimit(preset.getLimit()).queue();
|
||||
}
|
||||
if (preset.getBitrate() > 0) {
|
||||
channel.getManager().setBitrate(preset.getBitrate()).queue();
|
||||
}
|
||||
if (preset.getLimit() >= 0) channel.getManager().setUserLimit(preset.getLimit()).queue();
|
||||
if (preset.getBitrate() > 0) channel.getManager().setBitrate(preset.getBitrate()).queue();
|
||||
if (preset.isLocked()) {
|
||||
channel
|
||||
.upsertPermissionOverride(channel.getGuild().getPublicRole())
|
||||
.deny(Permission.VOICE_CONNECT)
|
||||
.queue();
|
||||
var publicRole = channel.getGuild().getPublicRole();
|
||||
channel.upsertPermissionOverride(publicRole).deny(Permission.VOICE_CONNECT).queue();
|
||||
}
|
||||
LoggerDecorator.voice(
|
||||
logger, "Применён пресет пользователя " + userId + " к каналу " + channel.getId());
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@ import creator.loki.embed.EmbedFactory;
|
|||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import creator.loki.util.GuildChannelResolve;
|
||||
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 {
|
||||
|
||||
private final VoiceChannelManager voiceChannelManager;
|
||||
private final ConfigRepository configRepo;
|
||||
|
||||
|
|
@ -48,13 +47,11 @@ public class VoiceSetupPrefixCommand implements Command {
|
|||
context.getChannel().sendMessage("❌ У вас нет прав администратора!").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> args = context.getArgs();
|
||||
var args = context.getArgs();
|
||||
if (args.isEmpty()) {
|
||||
sendHelp(context);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (args.get(0).toLowerCase()) {
|
||||
case "trigger" -> handleTrigger(context, args);
|
||||
case "category" -> handleCategory(context, args);
|
||||
|
|
@ -65,17 +62,12 @@ public class VoiceSetupPrefixCommand implements Command {
|
|||
|
||||
private void handleTrigger(CommandContext context, List<String> args) {
|
||||
if (args.size() < 2) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessage(
|
||||
"❌ Укажите голосовой канал: `#канал`, `<#id>` или числовой id."
|
||||
+ "\nПример: `!voice-setup trigger 123456789012345678`")
|
||||
.queue();
|
||||
var msg =
|
||||
"❌ Укажите голосовой канал: `#канал`, `<#id>` или числовой id.\nПример: `!voice-setup trigger 123456789012345678`";
|
||||
context.getChannel().sendMessage(msg).queue();
|
||||
return;
|
||||
}
|
||||
|
||||
VoiceChannel channel =
|
||||
GuildChannelResolve.voice(context.getGuild(), context.getMessage(), args, 1);
|
||||
var channel = GuildChannelResolve.voice(context.getGuild(), context.getMessage(), args, 1);
|
||||
if (channel == null) {
|
||||
context
|
||||
.getChannel()
|
||||
|
|
@ -85,34 +77,24 @@ public class VoiceSetupPrefixCommand implements Command {
|
|||
return;
|
||||
}
|
||||
configRepo.set("voice.trigger_channel_id", channel.getId());
|
||||
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Канал-триггер установлен")
|
||||
.withDescription(
|
||||
"Теперь при входе в "
|
||||
+ channel.getAsMention()
|
||||
+ " будет создаваться личный канал")
|
||||
.withColor(Color.GREEN)
|
||||
.build())
|
||||
.queue();
|
||||
var embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Канал-триггер установлен")
|
||||
.withDescription(
|
||||
"Теперь при входе в " + channel.getAsMention() + " будет создаваться личный канал")
|
||||
.withColor(Color.GREEN)
|
||||
.build();
|
||||
context.getChannel().sendMessageEmbeds(embed).queue();
|
||||
}
|
||||
|
||||
private void handleCategory(CommandContext context, List<String> args) {
|
||||
if (args.size() < 2) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessage(
|
||||
"❌ Укажите категорию: `#категория`, `<#id>` или числовой id."
|
||||
+ "\nПример: `!voice-setup category 123456789012345678`")
|
||||
.queue();
|
||||
var msg =
|
||||
"❌ Укажите категорию: `#категория`, `<#id>` или числовой id.\nПример: `!voice-setup category 123456789012345678`";
|
||||
context.getChannel().sendMessage(msg).queue();
|
||||
return;
|
||||
}
|
||||
|
||||
Category category =
|
||||
GuildChannelResolve.category(context.getGuild(), context.getMessage(), args, 1);
|
||||
var category = GuildChannelResolve.category(context.getGuild(), context.getMessage(), args, 1);
|
||||
if (category == null) {
|
||||
context
|
||||
.getChannel()
|
||||
|
|
@ -122,60 +104,41 @@ public class VoiceSetupPrefixCommand implements Command {
|
|||
return;
|
||||
}
|
||||
configRepo.set("voice.category_id", category.getId());
|
||||
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Категория установлена")
|
||||
.withDescription(
|
||||
"Личные каналы будут создаваться в категории: **" + category.getName() + "**")
|
||||
.withColor(Color.GREEN)
|
||||
.build())
|
||||
.queue();
|
||||
var embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Категория установлена")
|
||||
.withDescription(
|
||||
"Личные каналы будут создаваться в категории: **" + category.getName() + "**")
|
||||
.withColor(Color.GREEN)
|
||||
.build();
|
||||
context.getChannel().sendMessageEmbeds(embed).queue();
|
||||
}
|
||||
|
||||
private void handleInfo(CommandContext context) {
|
||||
String triggerId = configRepo.get("voice.trigger_channel_id");
|
||||
String categoryId = configRepo.get("voice.category_id");
|
||||
|
||||
var triggerId = configRepo.get("voice.trigger_channel_id");
|
||||
var categoryId = configRepo.get("voice.category_id");
|
||||
var embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle("🎤 Конфигурация системы личных каналов")
|
||||
.withColor(Color.BLUE);
|
||||
|
||||
if (triggerId != null) {
|
||||
embed.addField("Канал-триггер", "<#" + triggerId + ">", false);
|
||||
} else {
|
||||
embed.addField("Канал-триггер", "❌ Не установлен", false);
|
||||
}
|
||||
|
||||
if (triggerId != null) embed.addField("Канал-триггер", "<#" + triggerId + ">", false);
|
||||
else embed.addField("Канал-триггер", "❌ Не установлен", false);
|
||||
if (categoryId != null) {
|
||||
Category category = context.getGuild().getCategoryById(categoryId);
|
||||
var category = context.getGuild().getCategoryById(categoryId);
|
||||
embed.addField("Категория", category != null ? category.getName() : "❌ Не найдена", false);
|
||||
} else {
|
||||
embed.addField("Категория", "❌ Не установлена", false);
|
||||
}
|
||||
|
||||
} else embed.addField("Категория", "❌ Не установлена", false);
|
||||
context.getChannel().sendMessageEmbeds(embed.build()).queue();
|
||||
}
|
||||
|
||||
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
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("🎤 Настройка голосовых каналов")
|
||||
.withDescription(
|
||||
"""
|
||||
**Доступные команды:**
|
||||
• `!voice-setup trigger` - `#войс` или числовой id канала
|
||||
• `!voice-setup category` - `#категория` или id
|
||||
• `!voice-setup info` - Показать текущую конфигурацию
|
||||
|
||||
Текстовые каналы для управления создаются автоматически при создании войса.
|
||||
Только для администраторов!
|
||||
""")
|
||||
.withDescription(desc)
|
||||
.withColor(Color.BLUE)
|
||||
.build())
|
||||
.queue();
|
||||
|
|
|
|||
|
|
@ -1,57 +1,69 @@
|
|||
package creator.loki.integration.nekos;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
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.LoggerFactory;
|
||||
|
||||
/** Утилита для работы с nekos.life API */
|
||||
@Singleton
|
||||
public class NekosLifeApi {
|
||||
private static final Logger logger = LoggerFactory.getLogger(NekosLifeApi.class);
|
||||
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 с повторными попытками */
|
||||
public static String fetchImage(String category) {
|
||||
private static NekosLifeApi instance;
|
||||
|
||||
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 retryDelay = 1000;
|
||||
long retryDelay = 1000;
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
HttpRequest request =
|
||||
HttpRequest.newBuilder()
|
||||
.uri(URI.create(API_BASE + category))
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.GET()
|
||||
.build();
|
||||
Request request = new Request.Builder().url(API_BASE + category).get().build();
|
||||
|
||||
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
|
||||
return json.get("url").getAsString();
|
||||
} else if (response.statusCode() >= 500) {
|
||||
logger.warn(
|
||||
"Nekos.life API error: {} (attempt {}/{})",
|
||||
response.statusCode(),
|
||||
attempt,
|
||||
maxRetries);
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
if (response.code() >= 500) {
|
||||
logger.warn(
|
||||
"Nekos.life API error: {} (attempt {}/{})", response.code(), attempt, maxRetries);
|
||||
} else {
|
||||
logger.error("Nekos.life API error: {}", response.code());
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
logger.error("Nekos.life API error: {}", response.statusCode());
|
||||
return null;
|
||||
ResponseBody body = response.body();
|
||||
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) {
|
||||
Thread.currentThread().interrupt();
|
||||
logger.error("Interrupted while accessing nekos.life API", e);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
} catch (IOException e) {
|
||||
logger.error(
|
||||
"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);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String fetchImage(String category) {
|
||||
if (instance == null) {
|
||||
instance = new NekosLifeApi();
|
||||
}
|
||||
return instance.getImage(category);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
39
src/main/java/creator/loki/util/VoiceHelper.java
Normal file
39
src/main/java/creator/loki/util/VoiceHelper.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import creator.loki.feature.counter.repository.CounterStatsRepository;
|
|||
import creator.loki.feature.economy.EconomyCommandRegistrar;
|
||||
import creator.loki.feature.economy.service.*;
|
||||
import creator.loki.feature.entertainment.EntertainmentCommandRegistrar;
|
||||
import creator.loki.feature.entertainment.service.RatingService;
|
||||
import creator.loki.feature.level.LevelCommandRegistrar;
|
||||
import creator.loki.feature.level.LevelRoleService;
|
||||
import creator.loki.feature.level.ResetLevelsCommand;
|
||||
|
|
@ -101,7 +102,10 @@ class SmokeCommandRegistrationTest {
|
|||
|
||||
var entertainmentRegistrar =
|
||||
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);
|
||||
|
||||
var utilityRegistrar =
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||
import java.awt.Color;
|
||||
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.Test;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,16 +9,20 @@ import java.util.function.Consumer;
|
|||
|
||||
import creator.loki.command.CommandContext;
|
||||
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.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.Message;
|
||||
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.middleman.GuildChannel;
|
||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||
import net.dv8tion.jda.api.managers.channel.concrete.TextChannelManager;
|
||||
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.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
|
@ -35,7 +39,17 @@ class SlowmodeCommandTest {
|
|||
@Mock Member member;
|
||||
@Mock Member selfMember;
|
||||
@Mock Guild guild;
|
||||
@Mock JDA jda;
|
||||
@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 channelManager;
|
||||
|
||||
|
|
@ -95,7 +109,7 @@ class SlowmodeCommandTest {
|
|||
lenient().when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendEmbedAction);
|
||||
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
|
||||
.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)))
|
||||
.thenReturn(false);
|
||||
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(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
|
||||
.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)))
|
||||
.thenReturn(true);
|
||||
when(channel.getSlowmode()).thenReturn(10);
|
||||
|
|
@ -129,7 +143,7 @@ class SlowmodeCommandTest {
|
|||
when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendEmbedAction);
|
||||
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
|
||||
.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)))
|
||||
.thenReturn(true);
|
||||
|
||||
|
|
@ -147,7 +161,7 @@ class SlowmodeCommandTest {
|
|||
when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction);
|
||||
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
|
||||
.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)))
|
||||
.thenReturn(true);
|
||||
when(channel.getAsMention()).thenReturn("#general");
|
||||
|
|
@ -170,7 +184,7 @@ class SlowmodeCommandTest {
|
|||
when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction);
|
||||
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
|
||||
.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)))
|
||||
.thenReturn(true);
|
||||
|
||||
|
|
@ -187,7 +201,7 @@ class SlowmodeCommandTest {
|
|||
when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction);
|
||||
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
|
||||
.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)))
|
||||
.thenReturn(true);
|
||||
|
||||
|
|
@ -205,7 +219,7 @@ class SlowmodeCommandTest {
|
|||
when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction);
|
||||
when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL)))
|
||||
.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)))
|
||||
.thenReturn(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ class RankCardGeneratorTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void backgroundNotFoundThrowsException() {
|
||||
void backgroundNotFoundReturnsFallback() throws Exception {
|
||||
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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue