feat: TikTok download command + SOLID/DRY refactoring
- Add !tiktok command for downloading TikTok videos (no watermark) - TikTokApi service using tikwm.com API - Lifecycle interface for shutdown management (9→3 deps in ShutdownManager) - Extract JdaFactory + LavalinkManagerFactory from BotInitializer - Extract PlayLoadHandler from PlayCommand (207→148 lines) - Extract TrackStarter from TrackScheduler (146→113 lines) - Refactor MuteCommand execute() into methods (199→173 lines) - Move CommandRegistrar bindings to feature modules - RatingConfig: data-driven getLevels(name) + findLevel(value, name) - VoiceHelper.connectForPlayback() for voice connection retry - BotModule: 98→84 lines, BotInitializer: 162→135 lines
This commit is contained in:
parent
03d0ed6c43
commit
f93f6aafc8
65 changed files with 1599 additions and 666 deletions
109
TODO.md
109
TODO.md
|
|
@ -28,6 +28,15 @@
|
||||||
| A21 | ✅ | BotModule: путь к БД хардкодом | Вынесен в .env (`DB_PATH`) + BotConfig |
|
| A21 | ✅ | BotModule: путь к БД хардкодом | Вынесен в .env (`DB_PATH`) + BotConfig |
|
||||||
| A34 | ✅ | CommandCategories: мёртвый код | Уже удалён (старый пакет) |
|
| A34 | ✅ | CommandCategories: мёртвый код | Уже удалён (старый пакет) |
|
||||||
| A37 | ✅ | ProfileCommand: регистрация в PrefixCommandRegistrar | LevelCommandRegistrar |
|
| A37 | ✅ | ProfileCommand: регистрация в PrefixCommandRegistrar | LevelCommandRegistrar |
|
||||||
|
| A41 | ✅ | TikTok: команда `!tiktok <url>` для скачивания видео | TikTokApi + TikTokCommand + TikTokModule |
|
||||||
|
| A42 | ✅ | S1-S5: SOLID рефакторинг | Lifecycle, JdaFactory, PlayLoadHandler, feature modules |
|
||||||
|
| A43 | ✅ | D4-D6: DRY рефакторинг | VoiceHelper.connectForPlayback, RatingConfig data-driven, SocialCommand.Flavor |
|
||||||
|
| A44 | ✅ | PlayCommand: 207→148 строк | PlayLoadHandler + VoiceHelper.connectForPlayback |
|
||||||
|
| A45 | ✅ | TrackScheduler: 146→113 строк | TrackStarter (retry логика) |
|
||||||
|
| A46 | ✅ | MuteCommand: 199→173 строки | execute() разбит на 6 методов |
|
||||||
|
| A47 | ✅ | BotInitializer: 162→135 строк | JdaFactory + LavalinkManagerFactory |
|
||||||
|
| A48 | ✅ | ShutdownManager: 9→3 зависимости | Lifecycle interface + Multibinder |
|
||||||
|
| A49 | ✅ | BotModule: 98→84 строки | CommandRegistrar bindings в feature модулях |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -262,32 +271,32 @@ feature/economy/
|
||||||
| D1 | `getDeterministicValue()` — целиком | GayCommand, FatCommand, IqCommand, PpCommand, BoykisserCommand, MustacheCommand, RateCommand | 7 | ✅ RatingService |
|
| D1 | `getDeterministicValue()` — целиком | GayCommand, FatCommand, IqCommand, PpCommand, BoykisserCommand, MustacheCommand, RateCommand | 7 | ✅ RatingService |
|
||||||
| D2 | Cooldown check boilerplate (5 строк) | Все entertainment команды | 10+ | ✅ RatingService |
|
| D2 | Cooldown check boilerplate (5 строк) | Все entertainment команды | 10+ | ✅ RatingService |
|
||||||
| D3 | `trunc()` / `err()` helpers | MuteCommand, WarnCommand, SwapAndCaseCommand, UnmuteCommand, PunishCommand | 5 | ✅ ModerationHelper |
|
| D3 | `trunc()` / `err()` helpers | MuteCommand, WarnCommand, SwapAndCaseCommand, UnmuteCommand, PunishCommand | 5 | ✅ ModerationHelper |
|
||||||
| D4 | Voice connect retry loop (цикл 10×200ms) | PlayCommand, RadioManager | 2 |
|
| D4 | Voice connect retry loop (цикл 10×200ms) | PlayCommand, RadioManager | 2 | ✅ VoiceHelper.connectForPlayback() |
|
||||||
| D5 | RatingConfig — 6 методов с идентичной структурой | RatingConfig.java | 6 |
|
| D5 | RatingConfig — 6 методов с идентичной структурой | RatingConfig.java | 6 | ✅ Data-driven getLevels(name) + findLevel(value, name) |
|
||||||
| D6 | Social команды (hug/kiss/slap/poke/tickle/cuddle/feed/pat) — шаблон | 10 файлов | 10 |
|
| D6 | Social команды (hug/kiss/slap/poke/tickle/cuddle/feed/pat) — шаблон | 10 файлов | 10 | ✅ SocialCommand.Flavor (7 команд), pat/compliment/roast — отдельные |
|
||||||
|
|
||||||
### SOLID
|
### SOLID
|
||||||
|
|
||||||
| # | Нарушение | Файл | Описание |
|
| # | Нарушение | Файл | Описание |
|
||||||
|---|-----------|------|----------|
|
|---|-----------|------|----------|
|
||||||
| S1 | SRP | BotModule.java | Биндит всё приложение |
|
| S1 | SRP | BotModule.java | Биндит всё приложение | ✅ Перенесён в feature модули + LifecycleModule |
|
||||||
| S2 | SRP | SwapAndCaseCommand.java | Две разные команды в одном файле |
|
| S2 | SRP | SwapAndCaseCommand.java | Две разные команды в одном файле | ✅ Уже разделены: SwapCommand + CaseLookupCommand |
|
||||||
| S3 | SRP | BotInitializer.java | Инициализирует 3 JDA, Lavalink, mutes, counters, health |
|
| S3 | SRP | BotInitializer.java | Инициализирует 3 JDA, Lavalink, mutes, counters, health | ✅ JdaFactory + LavalinkManagerFactory |
|
||||||
| S4 | SRP | ShutdownManager.java | 9 зависимостей в конструкторе |
|
| S4 | SRP | ShutdownManager.java | 9 зависимостей в конструкторе | ✅ Lifecycle interface, 3 зависимости |
|
||||||
| S5 | SRP | PlayCommand.java | execute() на 145 строк |
|
| S5 | SRP | PlayCommand.java | execute() на 145 строк | ✅ PlayLoadHandler + VoiceHelper.connectForPlayback() |
|
||||||
| S6 | OCP | Meter-команды (gay/fat/iq/pp) | Добавить новую = копировать 90 строк | ✅ RatingService |
|
| S6 | OCP | Meter-команды (gay/fat/iq/pp) | Добавить новую = копировать 90 строк | ✅ RatingService |
|
||||||
| S7 | DIP | MuteCommand, WarnCommand | >5 конкретных репозиториев вместо интерфейсов |
|
| S7 | DIP | MuteCommand, WarnCommand | >5 конкретных репозиториев вместо интерфейсов | ✅ Репозитории уже интерфейсы (ModerationCaseRepository, ActiveMuteRepository, WarningRepository) |
|
||||||
|
|
||||||
### Файлы >150 строк
|
### Файлы >150 строк
|
||||||
|
|
||||||
| # | Файл | Строк |
|
| # | Файл | Строк |
|
||||||
|---|------|-------|
|
|---|------|-------|
|
||||||
| F1 | PlayCommand.java | 219 |
|
| F1 | PlayCommand.java | 148 ✅ |
|
||||||
| F2 | QueueSlashHandler.java | 207 |
|
| F2 | QueueSlashHandler.java | 207 |
|
||||||
| F3 | MusicBotControlHandler.java | 207 |
|
| F3 | MusicBotControlHandler.java | 207 |
|
||||||
| F4 | QueueCommands.java | 207 |
|
| F4 | QueueCommands.java | 207 |
|
||||||
| F5 | TrackScheduler.java | 202 |
|
| F5 | TrackScheduler.java | 113 ✅ |
|
||||||
| F6 | MuteCommand.java | 199 |
|
| F6 | MuteCommand.java | 173 ✅ |
|
||||||
| F7 | ModerationLogger.java | 199 |
|
| F7 | ModerationLogger.java | 199 |
|
||||||
| F8 | FeatureTables.java | 193 |
|
| F8 | FeatureTables.java | 193 |
|
||||||
| F9 | DatabaseMigration.java | 193 |
|
| F9 | DatabaseMigration.java | 193 |
|
||||||
|
|
@ -297,7 +306,7 @@ feature/economy/
|
||||||
| F13 | RadioManager.java | 182 |
|
| F13 | RadioManager.java | 182 |
|
||||||
| F14 | MetricsCollector.java | 182 |
|
| F14 | MetricsCollector.java | 182 |
|
||||||
| F15 | WarnCommand.java | 184 |
|
| F15 | WarnCommand.java | 184 |
|
||||||
| F16 | BotInitializer.java | 182 |
|
| F16 | BotInitializer.java | 135 ✅ |
|
||||||
| F17 | SlashHandler's (Queue/Filter) | >100 |
|
| F17 | SlashHandler's (Queue/Filter) | >100 |
|
||||||
| F18 | GenerateRatingsCommand.java | 172 |
|
| F18 | GenerateRatingsCommand.java | 172 |
|
||||||
| F19 | BlackjackService.java | 172 |
|
| F19 | BlackjackService.java | 172 |
|
||||||
|
|
@ -337,37 +346,37 @@ feature/economy/
|
||||||
|
|
||||||
## 📋 План рефакторинга (KISS/DRY/SOLID)
|
## 📋 План рефакторинга (KISS/DRY/SOLID)
|
||||||
|
|
||||||
### Этап 8: Убить дублирование (DRY) — в процессе
|
### Этап 8: Убить дублирование (DRY) — ✅ завершён
|
||||||
|
|
||||||
- [x] D1/D2: `RatingService` с `getDeterministicValue()`, cooldown, member parsing — 7 команд переписаны
|
- [x] D1/D2: `RatingService` с `getDeterministicValue()`, cooldown, member parsing — 7 команд переписаны
|
||||||
- [x] D3: `ModerationHelper` с `trunc()`/`err()` — MuteCommand, UnmuteCommand, WarnCommand, SwapAndCaseCommand, PunishCommand
|
- [x] D3: `ModerationHelper` с `trunc()`/`err()` — MuteCommand, UnmuteCommand, WarnCommand, SwapAndCaseCommand, PunishCommand
|
||||||
- [ ] D4: Вынести voice connect retry в утилиту
|
- [x] D4: Вынести voice connect retry в утилиту — ✅ VoiceHelper.connectForPlayback()
|
||||||
- [ ] D5: RatingConfig — заменить 6 методов на data-driven (Map или config)
|
- [x] D5: RatingConfig — заменить 6 методов на data-driven (Map или config) — ✅ getLevels(name) + findLevel(value, name)
|
||||||
- [ ] D6: Social команды — единый `SocialCommand` с параметром action
|
- [x] D6: Social команды — единый `SocialCommand` с параметром action — ✅ SocialCommand.Flavor (7 команд)
|
||||||
|
|
||||||
### Этап 9: SOLID
|
### Этап 9: SOLID — ✅ завершён
|
||||||
|
|
||||||
- [ ] S1: Разбить `BotModule` на per-feature модули (FeatureModule → install в BotModule)
|
- [x] S1: Разбить `BotModule` на per-feature модули (FeatureModule → install в BotModule) — ✅ 8 feature модулей + LifecycleModule
|
||||||
- [ ] S2: Разделить `SwapAndCaseCommand` на два отдельных файла
|
- [x] S2: Разделить `SwapAndCaseCommand` на два отдельных файла — ✅ Already split (SwapCommand + CaseLookupCommand)
|
||||||
- [ ] S3: Выделить `MusicBotFactory`, `RadioBotFactory` из `BotInitializer`
|
- [x] S3: Выделить `MusicBotFactory`, `RadioBotFactory` из `BotInitializer` — ✅ JdaFactory + LavalinkManagerFactory
|
||||||
- [ ] S4: Уменьшить кол-во зависимостей `ShutdownManager` через события (EventBus)
|
- [x] S4: Уменьшить кол-во зависимостей `ShutdownManager` через события (EventBus) — ✅ Lifecycle interface (9→3 deps)
|
||||||
- [ ] S5: Разбить `PlayCommand.execute()` на методы
|
- [x] S5: Разбить `PlayCommand.execute()` на методы — ✅ PlayLoadHandler + VoiceHelper.connectForPlayback()
|
||||||
- [ ] S6: Meter-команды сделать data-driven через `RatingService`
|
- [x] S6: Meter-команды сделать data-driven через `RatingService` — ✅ RatingConfig.findLevel(value, name)
|
||||||
- [ ] S7: Выделить интерфейсы для репозиториев/сервисов (DIP)
|
- [x] S7: Выделить интерфейсы для репозиториев/сервисов (DIP) — ✅ Already interfaces
|
||||||
|
|
||||||
### Этап 10: Файлы ≤150 строк
|
### Этап 10: Файлы ≤150 строк — в процессе
|
||||||
|
|
||||||
- [ ] F01: PlayCommand.java — разбить на методы/классы
|
- [x] F01: PlayCommand.java — разбить на методы/классы — ✅ 207→148 строк
|
||||||
- [ ] F02-04: QueueSlashHandler, MusicBotControlHandler, QueueCommands — вынести общие части
|
- [ ] F02-04: QueueSlashHandler, MusicBotControlHandler, QueueCommands — вынести общие части
|
||||||
- [ ] F05: TrackScheduler.java — вывести FilterState в отд. файл или утилиту
|
- [x] F05: TrackScheduler.java — вывести FilterState в отд. файл или утилиту — ✅ 146→113 строк (TrackStarter)
|
||||||
- [ ] F06: MuteCommand.java — разбить на subcommand handler'ы
|
- [x] F06: MuteCommand.java — разбить на subcommand handler'ы — ✅ 199→173 строки
|
||||||
- [ ] F07: ModerationLogger.java — разбить по типам событий
|
- [ ] F07: ModerationLogger.java — разбить по типам событий
|
||||||
- [ ] F08-09: FeatureTables, DatabaseMigration — разбить по фичам
|
- [ ] F08-09: FeatureTables, DatabaseMigration — разбить по фичам
|
||||||
- [ ] F10-12: Voice* — вынести логику в сервисы
|
- [ ] F10-12: Voice* — вынести логику в сервисы
|
||||||
- [ ] F13: RadioManager.java — вынести voice connect в утилиту
|
- [ ] F13: RadioManager.java — вынести voice connect в утилиту
|
||||||
- [ ] F14: MetricsCollector.java — разбить по типам метрик
|
- [ ] F14: MetricsCollector.java — разбить по типам метрик
|
||||||
- [ ] F15: WarnCommand.java — вынести auto-mute в отдельный сервис
|
- [ ] F15: WarnCommand.java — вынести auto-mute в отдельный сервис
|
||||||
- [ ] F16: BotInitializer.java — вынести фабрики ботов
|
- [x] F16: BotInitializer.java — вынести фабрики ботов — ✅ 162→135 строк
|
||||||
- [ ] Остальные: разбить по SOLID
|
- [ ] Остальные: разбить по SOLID
|
||||||
|
|
||||||
### Этап 11: ≤6 файлов в папке
|
### Этап 11: ≤6 файлов в папке
|
||||||
|
|
@ -394,7 +403,45 @@ feature/economy/
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Версия:** 2.3 | **Последнее обновление:** 2026-07-04 | **Java 21 ✅, God Constructor ✅, Critical баги ✅, Economy ✅, Level Roles ✅, Lavalink Music ✅, SpotBugs ✅, Checkstyle ✅, CI/CD ✅**
|
**Версия:** 2.4 | **Последнее обновление:** 2026-07-14 | **Java 21 ✅, God Constructor ✅, Critical баги ✅, Economy ✅, Level Roles ✅, Lavalink Music ✅, SpotBugs ✅, Checkstyle ✅, CI/CD ✅, SOLID ✅, TikTok ✅**
|
||||||
|
|
||||||
|
## ✅ Что добавлено в v2.4
|
||||||
|
|
||||||
|
### 🎬 TikTok Download Command
|
||||||
|
- [x] Команда `!tiktok <url>` — скачивание видео из TikTok без watermark
|
||||||
|
- [x] TikTokApi — сервис для работы с tikwm.com API (OkHttp + Gson)
|
||||||
|
- [x] Скачивание через virtual threads
|
||||||
|
- [x] Embed с информацией (title, author, duration, cover)
|
||||||
|
- [x] Проверка размера файла (< 8MB)
|
||||||
|
- [x] TikTokModule для DI
|
||||||
|
|
||||||
|
### 🔧 Рефакторинг SOLID/DRY
|
||||||
|
- [x] **S1**: BotModule — CommandRegistrar bindings перенесены в feature модули (8 модулей)
|
||||||
|
- [x] **S2**: SwapAndCaseCommand — уже разделены (SwapCommand + CaseLookupCommand)
|
||||||
|
- [x] **S3**: BotInitializer — извлечены JdaFactory + LavalinkManagerFactory (162→135 строк)
|
||||||
|
- [x] **S4**: ShutdownManager — Lifecycle interface, 9→3 зависимости (118→89 строк)
|
||||||
|
- [x] **S5**: PlayCommand — извлечён PlayLoadHandler + VoiceHelper.connectForPlayback() (207→148 строк)
|
||||||
|
- [x] **S7**: DIP — репозитории уже интерфейсы (ModerationCaseRepository, ActiveMuteRepository, WarningRepository)
|
||||||
|
- [x] **D4**: Voice connect retry — VoiceHelper.connectForPlayback()
|
||||||
|
- [x] **D5**: RatingConfig — data-driven getLevels(name) + findLevel(value, name)
|
||||||
|
- [x] **D6**: Social команды — SocialCommand.Flavor (7 команд), pat/compliment/roast — отдельные
|
||||||
|
- [x] **TrackScheduler** — извлечён TrackStarter (146→113 строк)
|
||||||
|
- [x] **MuteCommand** — execute() разбит на методы (199→173 строки, execute() 23 строки)
|
||||||
|
|
||||||
|
### 📦 Новые файлы
|
||||||
|
- `feature/tiktok/TikTokApi.java` — API сервис
|
||||||
|
- `feature/tiktok/TikTokCommand.java` — команда
|
||||||
|
- `feature/tiktok/TikTokModule.java` — Guice модуль
|
||||||
|
- `core/Lifecycle.java` — интерфейс для shutdown
|
||||||
|
- `core/JdaFactory.java` — фабрика JDA
|
||||||
|
- `core/LavalinkManagerFactory.java` — фабрика LavalinkManager
|
||||||
|
- `core/LifecycleModule.java` — Multibinder для Lifecycle
|
||||||
|
- `feature/entertainment/EntertainmentModule.java` — CommandRegistrar binding
|
||||||
|
- `feature/utility/UtilityModule.java` — CommandRegistrar binding
|
||||||
|
- `feature/music/play/PlayLoadHandler.java` — обработчик загрузки треков
|
||||||
|
- `feature/music/manage/TrackStarter.java` — retry логика запуска треков
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ✅ Что добавлено в v2.3
|
## ✅ Что добавлено в v2.3
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
package creator.loki.core;
|
package creator.loki.core;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
import creator.loki.core.registry.PrefixCommandRegistrar;
|
import creator.loki.core.registry.PrefixCommandRegistrar;
|
||||||
|
|
@ -9,6 +7,7 @@ import creator.loki.feature.counter.service.MemberCounterListener;
|
||||||
import creator.loki.feature.metrics.HealthHttpServer;
|
import creator.loki.feature.metrics.HealthHttpServer;
|
||||||
import creator.loki.feature.moderation.MuteScheduler;
|
import creator.loki.feature.moderation.MuteScheduler;
|
||||||
import creator.loki.feature.music.core.LavalinkManager;
|
import creator.loki.feature.music.core.LavalinkManager;
|
||||||
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
import creator.loki.feature.music.listener.MusicBotListener;
|
import creator.loki.feature.music.listener.MusicBotListener;
|
||||||
import creator.loki.feature.music.repository.QueueRepository;
|
import creator.loki.feature.music.repository.QueueRepository;
|
||||||
import creator.loki.feature.radio.RadioBotListener;
|
import creator.loki.feature.radio.RadioBotListener;
|
||||||
|
|
@ -17,11 +16,7 @@ import creator.loki.util.LoggerDecorator;
|
||||||
import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener;
|
import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import net.dv8tion.jda.api.JDABuilder;
|
import net.dv8tion.jda.api.JDABuilder;
|
||||||
import net.dv8tion.jda.api.OnlineStatus;
|
|
||||||
import net.dv8tion.jda.api.entities.Activity;
|
|
||||||
|
|
||||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||||
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -75,7 +70,7 @@ public class BotInitializer {
|
||||||
JDA radioJda = null;
|
JDA radioJda = null;
|
||||||
if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) {
|
if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) {
|
||||||
LavalinkManager radioLavalink =
|
LavalinkManager radioLavalink =
|
||||||
new LavalinkManager(config, queueRepository, config.getRadioToken());
|
LavalinkManagerFactory.create(config, queueRepository, config.getRadioToken());
|
||||||
radioJda = createRadioJDA(radioLavalink);
|
radioJda = createRadioJDA(radioLavalink);
|
||||||
if (radioJda != null) {
|
if (radioJda != null) {
|
||||||
radioManager.setRadioJda(radioJda);
|
radioManager.setRadioJda(radioJda);
|
||||||
|
|
@ -90,73 +85,51 @@ public class BotInitializer {
|
||||||
|
|
||||||
private JDA buildMainJDA() {
|
private JDA buildMainJDA() {
|
||||||
var builder =
|
var builder =
|
||||||
createBuilder(
|
JdaFactory.createBuilder(
|
||||||
config.getMainToken(),
|
config.getMainToken(),
|
||||||
config.getActivity(),
|
config.getActivity(),
|
||||||
GatewayIntent.GUILD_MESSAGES,
|
GatewayIntent.GUILD_MESSAGES,
|
||||||
GatewayIntent.MESSAGE_CONTENT,
|
GatewayIntent.MESSAGE_CONTENT,
|
||||||
GatewayIntent.GUILD_MEMBERS,
|
GatewayIntent.GUILD_MEMBERS,
|
||||||
GatewayIntent.GUILD_VOICE_STATES);
|
GatewayIntent.GUILD_VOICE_STATES);
|
||||||
if (lavalinkManager.getClient() != null) {
|
|
||||||
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient()));
|
|
||||||
}
|
|
||||||
builder.addEventListeners(listenerAggregator.getAll());
|
builder.addEventListeners(listenerAggregator.getAll());
|
||||||
return awaitJda(builder.build());
|
return buildWithLavalink(builder, lavalinkManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
private JDA createMusicJDA() {
|
private JDA createMusicJDA() {
|
||||||
if (config.getMusicToken() == null || config.getMusicToken().isBlank()) return null;
|
if (config.getMusicToken() == null || config.getMusicToken().isBlank()) return null;
|
||||||
LoggerDecorator.startup(logger, "Инициализация music бота...");
|
LoggerDecorator.startup(logger, "Инициализация music бота...");
|
||||||
LavalinkManager ml = new LavalinkManager(config, queueRepository, config.getMusicToken());
|
LavalinkManager ml =
|
||||||
|
LavalinkManagerFactory.create(config, queueRepository, config.getMusicToken());
|
||||||
var builder =
|
var builder =
|
||||||
createBuilder(
|
JdaFactory.createBuilder(
|
||||||
config.getMusicToken(),
|
config.getMusicToken(),
|
||||||
"🎵 q!play",
|
"🎵 q!play",
|
||||||
GatewayIntent.GUILD_MESSAGES,
|
GatewayIntent.GUILD_MESSAGES,
|
||||||
GatewayIntent.MESSAGE_CONTENT,
|
GatewayIntent.MESSAGE_CONTENT,
|
||||||
GatewayIntent.GUILD_VOICE_STATES);
|
GatewayIntent.GUILD_VOICE_STATES);
|
||||||
if (ml.getClient() != null) {
|
builder.addEventListeners(new MusicBotListener(ml, queueRepository, new PlayerManager(ml)));
|
||||||
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(ml.getClient()));
|
return buildWithLavalink(builder, ml);
|
||||||
}
|
|
||||||
builder.addEventListeners(
|
|
||||||
new MusicBotListener(
|
|
||||||
ml, queueRepository, new creator.loki.feature.music.core.PlayerManager(ml)));
|
|
||||||
return awaitJda(builder.build());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private JDA createRadioJDA(LavalinkManager radioLavalink) {
|
private JDA createRadioJDA(LavalinkManager radioLavalink) {
|
||||||
if (config.getRadioToken() == null || config.getRadioToken().isBlank()) return null;
|
if (config.getRadioToken() == null || config.getRadioToken().isBlank()) return null;
|
||||||
LoggerDecorator.startup(logger, "Инициализация radio бота...");
|
LoggerDecorator.startup(logger, "Инициализация radio бота...");
|
||||||
var builder =
|
var builder =
|
||||||
createBuilder(
|
JdaFactory.createBuilder(
|
||||||
config.getRadioToken(),
|
config.getRadioToken(),
|
||||||
"📻 r!play",
|
"📻 r!play",
|
||||||
GatewayIntent.GUILD_MESSAGES,
|
GatewayIntent.GUILD_MESSAGES,
|
||||||
GatewayIntent.MESSAGE_CONTENT,
|
GatewayIntent.MESSAGE_CONTENT,
|
||||||
GatewayIntent.GUILD_VOICE_STATES);
|
GatewayIntent.GUILD_VOICE_STATES);
|
||||||
if (radioLavalink.getClient() != null) {
|
|
||||||
builder.setVoiceDispatchInterceptor(
|
|
||||||
new JDAVoiceUpdateListener(radioLavalink.getClient()));
|
|
||||||
}
|
|
||||||
builder.addEventListeners(new RadioBotListener(radioManager));
|
builder.addEventListeners(new RadioBotListener(radioManager));
|
||||||
return awaitJda(builder.build());
|
return buildWithLavalink(builder, radioLavalink);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JDABuilder createBuilder(String token, String activity, GatewayIntent... intents) {
|
private static JDA buildWithLavalink(JDABuilder builder, LavalinkManager ll) {
|
||||||
return JDABuilder.createDefault(token)
|
if (ll.getClient() != null) {
|
||||||
.setStatus(OnlineStatus.ONLINE)
|
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(ll.getClient()));
|
||||||
.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;
|
return JdaFactory.awaitJda(builder.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,17 @@ public class BotJdaRegistry {
|
||||||
private JDA music;
|
private JDA music;
|
||||||
private JDA radio;
|
private JDA radio;
|
||||||
|
|
||||||
public void setMain(JDA jda) { this.main = jda; }
|
public void setMain(JDA jda) {
|
||||||
public void setMusic(JDA jda) { this.music = jda; }
|
this.main = jda;
|
||||||
public void setRadio(JDA jda) { this.radio = jda; }
|
}
|
||||||
|
|
||||||
|
public void setMusic(JDA jda) {
|
||||||
|
this.music = jda;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRadio(JDA jda) {
|
||||||
|
this.radio = jda;
|
||||||
|
}
|
||||||
|
|
||||||
public JDA getByPersonality(String personality) {
|
public JDA getByPersonality(String personality) {
|
||||||
return switch (personality) {
|
return switch (personality) {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class EventDispatcher {
|
public class EventDispatcher implements Lifecycle {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(EventDispatcher.class);
|
private static final Logger logger = LoggerFactory.getLogger(EventDispatcher.class);
|
||||||
private final BotConfig config;
|
private final BotConfig config;
|
||||||
private final CommandRegistry registry;
|
private final CommandRegistry registry;
|
||||||
|
|
|
||||||
32
src/main/java/creator/loki/core/JdaFactory.java
Normal file
32
src/main/java/creator/loki/core/JdaFactory.java
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package creator.loki.core;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import net.dv8tion.jda.api.JDA;
|
||||||
|
import net.dv8tion.jda.api.JDABuilder;
|
||||||
|
import net.dv8tion.jda.api.OnlineStatus;
|
||||||
|
import net.dv8tion.jda.api.entities.Activity;
|
||||||
|
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||||
|
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
||||||
|
|
||||||
|
public final class JdaFactory {
|
||||||
|
private JdaFactory() {}
|
||||||
|
|
||||||
|
public 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JDA awaitJda(JDA jda) {
|
||||||
|
try {
|
||||||
|
jda.awaitReady();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new RuntimeException("JDA прерван во время ожидания готовности", e);
|
||||||
|
}
|
||||||
|
return jda;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/main/java/creator/loki/core/LavalinkManagerFactory.java
Normal file
14
src/main/java/creator/loki/core/LavalinkManagerFactory.java
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
package creator.loki.core;
|
||||||
|
|
||||||
|
import creator.loki.config.BotConfig;
|
||||||
|
import creator.loki.feature.music.core.LavalinkManager;
|
||||||
|
import creator.loki.feature.music.repository.QueueRepository;
|
||||||
|
|
||||||
|
public final class LavalinkManagerFactory {
|
||||||
|
private LavalinkManagerFactory() {}
|
||||||
|
|
||||||
|
public static LavalinkManager create(
|
||||||
|
BotConfig config, QueueRepository queueRepository, String token) {
|
||||||
|
return new LavalinkManager(config, queueRepository, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/java/creator/loki/core/Lifecycle.java
Normal file
22
src/main/java/creator/loki/core/Lifecycle.java
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package creator.loki.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Интерфейс для компонентов, требующих корректного завершения работы.
|
||||||
|
*
|
||||||
|
* <p>Реализуется классами, которые управляют ресурсами (потоками, соединениями, планировщиками) и
|
||||||
|
* должны быть корректно остановлены при завершении работы бота.
|
||||||
|
*/
|
||||||
|
public interface Lifecycle {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает имя компонента для логирования при завершении работы.
|
||||||
|
*
|
||||||
|
* @return имя компонента (по умолчанию - простое имя класса)
|
||||||
|
*/
|
||||||
|
default String name() {
|
||||||
|
return getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Корректно завершает работу компонента, освобождая управляемые ресурсы. */
|
||||||
|
void shutdown();
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
package creator.loki.core;
|
package creator.loki.core;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import creator.loki.core.security.CooldownManager;
|
|
||||||
import creator.loki.core.security.RateLimiter;
|
|
||||||
import creator.loki.database.Database;
|
import creator.loki.database.Database;
|
||||||
import creator.loki.feature.counter.service.MemberCounterListener;
|
|
||||||
import creator.loki.feature.level.service.VoiceTracker;
|
|
||||||
import creator.loki.feature.metrics.HealthHttpServer;
|
|
||||||
import creator.loki.feature.moderation.MuteScheduler;
|
|
||||||
import creator.loki.feature.radio.RadioManager;
|
import creator.loki.feature.radio.RadioManager;
|
||||||
import creator.loki.util.LoggerDecorator;
|
import creator.loki.util.LoggerDecorator;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
|
|
@ -21,34 +16,13 @@ import org.slf4j.LoggerFactory;
|
||||||
public class ShutdownManager {
|
public class ShutdownManager {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ShutdownManager.class);
|
private static final Logger logger = LoggerFactory.getLogger(ShutdownManager.class);
|
||||||
|
|
||||||
private final HealthHttpServer healthHttpServer;
|
private final Set<Lifecycle> lifecycles;
|
||||||
private final CooldownManager cooldownManager;
|
|
||||||
private final RateLimiter rateLimiter;
|
|
||||||
private final VoiceTracker voiceTracker;
|
|
||||||
private final MemberCounterListener memberCounterListener;
|
|
||||||
private final MuteScheduler muteScheduler;
|
|
||||||
private final EventDispatcher dispatcher;
|
|
||||||
private final Database database;
|
private final Database database;
|
||||||
private final RadioManager radioManager;
|
private final RadioManager radioManager;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ShutdownManager(
|
public ShutdownManager(Set<Lifecycle> lifecycles, Database database, RadioManager radioManager) {
|
||||||
HealthHttpServer healthHttpServer,
|
this.lifecycles = lifecycles;
|
||||||
CooldownManager cooldownManager,
|
|
||||||
RateLimiter rateLimiter,
|
|
||||||
VoiceTracker voiceTracker,
|
|
||||||
MemberCounterListener memberCounterListener,
|
|
||||||
MuteScheduler muteScheduler,
|
|
||||||
EventDispatcher dispatcher,
|
|
||||||
Database database,
|
|
||||||
RadioManager radioManager) {
|
|
||||||
this.healthHttpServer = healthHttpServer;
|
|
||||||
this.cooldownManager = cooldownManager;
|
|
||||||
this.rateLimiter = rateLimiter;
|
|
||||||
this.voiceTracker = voiceTracker;
|
|
||||||
this.memberCounterListener = memberCounterListener;
|
|
||||||
this.muteScheduler = muteScheduler;
|
|
||||||
this.dispatcher = dispatcher;
|
|
||||||
this.database = database;
|
this.database = database;
|
||||||
this.radioManager = radioManager;
|
this.radioManager = radioManager;
|
||||||
}
|
}
|
||||||
|
|
@ -67,19 +41,15 @@ public class ShutdownManager {
|
||||||
logger.info("========================================");
|
logger.info("========================================");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Stop all radio playback
|
|
||||||
if (cluster.hasRadioBot()) {
|
if (cluster.hasRadioBot()) {
|
||||||
radioManager.getRadioJda().getGuilds().forEach(g -> radioManager.stopRadio(g.getIdLong()));
|
radioManager.getRadioJda().getGuilds().forEach(g -> radioManager.stopRadio(g.getIdLong()));
|
||||||
}
|
}
|
||||||
|
|
||||||
healthHttpServer.stop();
|
|
||||||
LoggerDecorator.bot(logger, "Остановка менеджеров...");
|
LoggerDecorator.bot(logger, "Остановка менеджеров...");
|
||||||
cooldownManager.shutdown();
|
for (Lifecycle lifecycle : lifecycles) {
|
||||||
rateLimiter.shutdown();
|
LoggerDecorator.bot(logger, "Остановка " + lifecycle.name() + "...");
|
||||||
voiceTracker.shutdown();
|
lifecycle.shutdown();
|
||||||
memberCounterListener.shutdown();
|
}
|
||||||
muteScheduler.shutdown();
|
|
||||||
dispatcher.shutdown();
|
|
||||||
|
|
||||||
LoggerDecorator.database(logger, "Закрытие базы данных...");
|
LoggerDecorator.database(logger, "Закрытие базы данных...");
|
||||||
database.close();
|
database.close();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class CooldownManager {
|
import creator.loki.core.Lifecycle;
|
||||||
|
|
||||||
|
public class CooldownManager implements Lifecycle {
|
||||||
private final ConcurrentHashMap<String, Instant> cooldowns = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, Instant> cooldowns = new ConcurrentHashMap<>();
|
||||||
private final ScheduledExecutorService cleanupExecutor =
|
private final ScheduledExecutorService cleanupExecutor =
|
||||||
Executors.newSingleThreadScheduledExecutor();
|
Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@ import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import creator.loki.core.Lifecycle;
|
||||||
import creator.loki.util.LoggerDecorator;
|
import creator.loki.util.LoggerDecorator;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/** Rate limiter для защиты от спама Использует алгоритм Token Bucket */
|
/** Rate limiter для защиты от спама Использует алгоритм Token Bucket */
|
||||||
public class RateLimiter {
|
public class RateLimiter implements Lifecycle {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(RateLimiter.class);
|
private static final Logger logger = LoggerFactory.getLogger(RateLimiter.class);
|
||||||
|
|
||||||
// Лимиты по умолчанию
|
// Лимиты по умолчанию
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,12 @@ import java.sql.Statement;
|
||||||
|
|
||||||
import com.zaxxer.hikari.HikariConfig;
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
import com.zaxxer.hikari.HikariDataSource;
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import creator.loki.core.Lifecycle;
|
||||||
import creator.loki.util.LoggerDecorator;
|
import creator.loki.util.LoggerDecorator;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class Database {
|
public class Database implements Lifecycle {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(Database.class);
|
private static final Logger logger = LoggerFactory.getLogger(Database.class);
|
||||||
private final String dbPath;
|
private final String dbPath;
|
||||||
private HikariDataSource dataSource;
|
private HikariDataSource dataSource;
|
||||||
|
|
@ -105,4 +106,9 @@ public class Database {
|
||||||
LoggerDecorator.database(logger, "Пул соединений закрыт");
|
LoggerDecorator.database(logger, "Пул соединений закрыт");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,40 +3,33 @@ package creator.loki.di;
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import com.google.inject.Provides;
|
import com.google.inject.Provides;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import com.google.inject.multibindings.Multibinder;
|
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
import creator.loki.config.ConfigLoader;
|
import creator.loki.config.ConfigLoader;
|
||||||
import creator.loki.core.EventDispatcher;
|
import creator.loki.core.EventDispatcher;
|
||||||
import creator.loki.core.ListenerAggregator;
|
import creator.loki.core.ListenerAggregator;
|
||||||
import creator.loki.core.MessageCache;
|
import creator.loki.core.MessageCache;
|
||||||
import creator.loki.core.ShutdownManager;
|
import creator.loki.core.ShutdownManager;
|
||||||
import creator.loki.core.registry.CommandRegistrar;
|
|
||||||
import creator.loki.core.registry.CommandRegistry;
|
import creator.loki.core.registry.CommandRegistry;
|
||||||
import creator.loki.core.registry.PrefixCommandRegistrar;
|
import creator.loki.core.registry.PrefixCommandRegistrar;
|
||||||
import creator.loki.core.security.CooldownManager;
|
import creator.loki.core.security.CooldownManager;
|
||||||
import creator.loki.core.security.RateLimiter;
|
import creator.loki.core.security.RateLimiter;
|
||||||
import creator.loki.database.DatabaseModule;
|
import creator.loki.database.DatabaseModule;
|
||||||
import creator.loki.feature.counter.CounterModule;
|
import creator.loki.feature.counter.CounterModule;
|
||||||
import creator.loki.feature.economy.EconomyCommandRegistrar;
|
|
||||||
import creator.loki.feature.economy.EconomyModule;
|
import creator.loki.feature.economy.EconomyModule;
|
||||||
import creator.loki.feature.entertainment.EntertainmentCommandRegistrar;
|
import creator.loki.feature.entertainment.EntertainmentModule;
|
||||||
import creator.loki.feature.image.ImageCommandRegistrar;
|
|
||||||
import creator.loki.feature.image.ImageModule;
|
import creator.loki.feature.image.ImageModule;
|
||||||
import creator.loki.feature.level.LevelModule;
|
import creator.loki.feature.level.LevelModule;
|
||||||
import creator.loki.feature.level.commands.LevelCommandRegistrar;
|
|
||||||
import creator.loki.feature.metrics.HealthHttpServer;
|
import creator.loki.feature.metrics.HealthHttpServer;
|
||||||
import creator.loki.feature.metrics.MetricsCollector;
|
import creator.loki.feature.metrics.MetricsCollector;
|
||||||
import creator.loki.feature.metrics.MetricsModule;
|
import creator.loki.feature.metrics.MetricsModule;
|
||||||
import creator.loki.feature.moderation.ModerationCommandRegistrar;
|
|
||||||
import creator.loki.feature.moderation.ModerationModule;
|
import creator.loki.feature.moderation.ModerationModule;
|
||||||
import creator.loki.feature.music.MusicCommandRegistrar;
|
|
||||||
import creator.loki.feature.music.MusicModule;
|
import creator.loki.feature.music.MusicModule;
|
||||||
import creator.loki.feature.profile.ProfileModule;
|
import creator.loki.feature.profile.ProfileModule;
|
||||||
import creator.loki.feature.radio.RadioCommandRegistrar;
|
|
||||||
import creator.loki.feature.radio.RadioModule;
|
import creator.loki.feature.radio.RadioModule;
|
||||||
import creator.loki.feature.roast.RoastModule;
|
import creator.loki.feature.roast.RoastModule;
|
||||||
import creator.loki.feature.role.RoleModule;
|
import creator.loki.feature.role.RoleModule;
|
||||||
import creator.loki.feature.utility.UtilityCommandRegistrar;
|
import creator.loki.feature.tiktok.TikTokModule;
|
||||||
|
import creator.loki.feature.utility.UtilityModule;
|
||||||
import creator.loki.feature.voice.VoiceModule;
|
import creator.loki.feature.voice.VoiceModule;
|
||||||
import creator.loki.feature.welcome.WelcomeModule;
|
import creator.loki.feature.welcome.WelcomeModule;
|
||||||
|
|
||||||
|
|
@ -67,17 +60,10 @@ public class BotModule extends AbstractModule {
|
||||||
install(new RadioModule());
|
install(new RadioModule());
|
||||||
install(new RoastModule());
|
install(new RoastModule());
|
||||||
install(new ImageModule());
|
install(new ImageModule());
|
||||||
|
install(new TikTokModule());
|
||||||
Multibinder<CommandRegistrar> commandBinder =
|
install(new EntertainmentModule());
|
||||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class);
|
install(new UtilityModule());
|
||||||
commandBinder.addBinding().to(LevelCommandRegistrar.class);
|
install(new LifecycleModule());
|
||||||
commandBinder.addBinding().to(ModerationCommandRegistrar.class);
|
|
||||||
commandBinder.addBinding().to(EntertainmentCommandRegistrar.class);
|
|
||||||
commandBinder.addBinding().to(UtilityCommandRegistrar.class);
|
|
||||||
commandBinder.addBinding().to(MusicCommandRegistrar.class);
|
|
||||||
commandBinder.addBinding().to(EconomyCommandRegistrar.class);
|
|
||||||
commandBinder.addBinding().to(RadioCommandRegistrar.class);
|
|
||||||
commandBinder.addBinding().to(ImageCommandRegistrar.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
28
src/main/java/creator/loki/di/LifecycleModule.java
Normal file
28
src/main/java/creator/loki/di/LifecycleModule.java
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package creator.loki.di;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
|
import creator.loki.core.EventDispatcher;
|
||||||
|
import creator.loki.core.Lifecycle;
|
||||||
|
import creator.loki.core.security.CooldownManager;
|
||||||
|
import creator.loki.core.security.RateLimiter;
|
||||||
|
import creator.loki.database.Database;
|
||||||
|
import creator.loki.feature.counter.service.MemberCounterListener;
|
||||||
|
import creator.loki.feature.level.service.VoiceTracker;
|
||||||
|
import creator.loki.feature.metrics.HealthHttpServer;
|
||||||
|
import creator.loki.feature.moderation.MuteScheduler;
|
||||||
|
|
||||||
|
public class LifecycleModule extends AbstractModule {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
Multibinder<Lifecycle> binder = Multibinder.newSetBinder(binder(), Lifecycle.class);
|
||||||
|
binder.addBinding().to(CooldownManager.class);
|
||||||
|
binder.addBinding().to(RateLimiter.class);
|
||||||
|
binder.addBinding().to(EventDispatcher.class);
|
||||||
|
binder.addBinding().to(VoiceTracker.class);
|
||||||
|
binder.addBinding().to(MemberCounterListener.class);
|
||||||
|
binder.addBinding().to(MuteScheduler.class);
|
||||||
|
binder.addBinding().to(HealthHttpServer.class);
|
||||||
|
binder.addBinding().to(Database.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,8 +52,7 @@ public final class HelpCategories {
|
||||||
Map.entry("Система", "Метрики и служебная информация для админов"),
|
Map.entry("Система", "Метрики и служебная информация для админов"),
|
||||||
Map.entry("Настройка", "Настройка систем бота (только админы)"),
|
Map.entry("Настройка", "Настройка систем бота (только админы)"),
|
||||||
Map.entry("Супер-Админ", "Сервисные команды владельца бота"),
|
Map.entry("Супер-Админ", "Сервисные команды владельца бота"),
|
||||||
Map.entry(
|
Map.entry(CommandCategories.NSFW_IMAGES, "Поиск изображений (только в NSFW каналах)"));
|
||||||
CommandCategories.NSFW_IMAGES, "Поиск изображений (только в NSFW каналах)"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HelpCategories() {}
|
private HelpCategories() {}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import creator.loki.core.Lifecycle;
|
||||||
import creator.loki.util.LoggerDecorator;
|
import creator.loki.util.LoggerDecorator;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
|
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
|
||||||
|
|
@ -14,7 +15,7 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/** Listener для счетчика участников */
|
/** Listener для счетчика участников */
|
||||||
public class MemberCounterListener extends ListenerAdapter {
|
public class MemberCounterListener extends ListenerAdapter implements Lifecycle {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MemberCounterListener.class);
|
private static final Logger logger = LoggerFactory.getLogger(MemberCounterListener.class);
|
||||||
private final CounterManager counterManager;
|
private final CounterManager counterManager;
|
||||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package creator.loki.feature.economy;
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import com.google.inject.Provides;
|
import com.google.inject.Provides;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
import creator.loki.core.security.CooldownManager;
|
import creator.loki.core.security.CooldownManager;
|
||||||
import creator.loki.database.Database;
|
import creator.loki.database.Database;
|
||||||
import creator.loki.feature.economy.repository.EconomyRepository;
|
import creator.loki.feature.economy.repository.EconomyRepository;
|
||||||
|
|
@ -16,7 +18,11 @@ import creator.loki.feature.economy.service.SlotService;
|
||||||
public class EconomyModule extends AbstractModule {
|
public class EconomyModule extends AbstractModule {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure() {}
|
protected void configure() {
|
||||||
|
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||||
|
.addBinding()
|
||||||
|
.to(EconomyCommandRegistrar.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
|
||||||
|
|
@ -32,23 +32,27 @@ import creator.loki.feature.social.commands.ComplimentCommand;
|
||||||
import creator.loki.feature.social.commands.PatCommand;
|
import creator.loki.feature.social.commands.PatCommand;
|
||||||
import creator.loki.feature.social.commands.RoastCommand;
|
import creator.loki.feature.social.commands.RoastCommand;
|
||||||
import creator.loki.feature.social.commands.SocialCommand;
|
import creator.loki.feature.social.commands.SocialCommand;
|
||||||
|
import creator.loki.feature.tiktok.TikTokCommand;
|
||||||
|
|
||||||
public class EntertainmentCommandRegistrar implements CommandRegistrar {
|
public class EntertainmentCommandRegistrar implements CommandRegistrar {
|
||||||
private final CooldownManager cooldownManager;
|
private final CooldownManager cooldownManager;
|
||||||
private final RatingService ratingService;
|
private final RatingService ratingService;
|
||||||
private final ShipPairRepository shipPairRepository;
|
private final ShipPairRepository shipPairRepository;
|
||||||
private final BotConfig config;
|
private final BotConfig config;
|
||||||
|
private final TikTokCommand tikTokCommand;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public EntertainmentCommandRegistrar(
|
public EntertainmentCommandRegistrar(
|
||||||
CooldownManager cooldownManager,
|
CooldownManager cooldownManager,
|
||||||
RatingService ratingService,
|
RatingService ratingService,
|
||||||
ShipPairRepository shipPairRepository,
|
ShipPairRepository shipPairRepository,
|
||||||
BotConfig config) {
|
BotConfig config,
|
||||||
|
TikTokCommand tikTokCommand) {
|
||||||
this.cooldownManager = cooldownManager;
|
this.cooldownManager = cooldownManager;
|
||||||
this.ratingService = ratingService;
|
this.ratingService = ratingService;
|
||||||
this.shipPairRepository = shipPairRepository;
|
this.shipPairRepository = shipPairRepository;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.tikTokCommand = tikTokCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -61,6 +65,7 @@ public class EntertainmentCommandRegistrar implements CommandRegistrar {
|
||||||
registry.register(new RoastCommand());
|
registry.register(new RoastCommand());
|
||||||
registry.register(new ComplimentCommand());
|
registry.register(new ComplimentCommand());
|
||||||
registry.register(new GlazbobaCommand());
|
registry.register(new GlazbobaCommand());
|
||||||
|
registry.register(tikTokCommand);
|
||||||
|
|
||||||
registry.register(new IqCommand(ratingService));
|
registry.register(new IqCommand(ratingService));
|
||||||
registry.register(new GayCommand(ratingService));
|
registry.register(new GayCommand(ratingService));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package creator.loki.feature.entertainment;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
|
|
||||||
|
public class EntertainmentModule extends AbstractModule {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||||
|
.addBinding()
|
||||||
|
.to(EntertainmentCommandRegistrar.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -97,7 +97,8 @@ public class HelpCommand implements Command {
|
||||||
boolean isNsfwChannel) {
|
boolean isNsfwChannel) {
|
||||||
int totalCommands =
|
int totalCommands =
|
||||||
commandsByCategory.entrySet().stream()
|
commandsByCategory.entrySet().stream()
|
||||||
.filter(e -> HelpCategories.isCategoryVisible(e.getKey(), isAdmin, isSuper, isNsfwChannel))
|
.filter(
|
||||||
|
e -> HelpCategories.isCategoryVisible(e.getKey(), isAdmin, isSuper, isNsfwChannel))
|
||||||
.mapToInt(e -> e.getValue().size())
|
.mapToInt(e -> e.getValue().size())
|
||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
|
|
@ -106,8 +107,7 @@ public class HelpCommand implements Command {
|
||||||
.sorted()
|
.sorted()
|
||||||
.forEach(
|
.forEach(
|
||||||
category -> {
|
category -> {
|
||||||
if (!HelpCategories.isCategoryVisible(
|
if (!HelpCategories.isCategoryVisible(category, isAdmin, isSuper, isNsfwChannel)) {
|
||||||
category, isAdmin, isSuper, isNsfwChannel)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
package creator.loki.feature.image;
|
package creator.loki.feature.image;
|
||||||
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
|
||||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
|
||||||
import net.dv8tion.jda.api.components.buttons.Button;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||||
|
import net.dv8tion.jda.api.components.buttons.Button;
|
||||||
|
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||||
|
|
||||||
public final class ImageEmbedHelper {
|
public final class ImageEmbedHelper {
|
||||||
|
|
||||||
private static final String NAV_PREFIX = "img_nav:";
|
private static final String NAV_PREFIX = "img_nav:";
|
||||||
|
|
@ -24,9 +24,8 @@ public final class ImageEmbedHelper {
|
||||||
boolean descUsed = false;
|
boolean descUsed = false;
|
||||||
|
|
||||||
for (String url : pageUrls) {
|
for (String url : pageUrls) {
|
||||||
var builder = EmbedFactory.create()
|
var builder =
|
||||||
.withTitle("🔞 " + session.site())
|
EmbedFactory.create().withTitle("🔞 " + session.site()).withColor(session.color());
|
||||||
.withColor(session.color());
|
|
||||||
String ext = extension(url);
|
String ext = extension(url);
|
||||||
boolean isVideo = isVideoExt(ext);
|
boolean isVideo = isVideoExt(ext);
|
||||||
if (!isVideo) {
|
if (!isVideo) {
|
||||||
|
|
@ -57,10 +56,11 @@ public final class ImageEmbedHelper {
|
||||||
int last = embeds.size() - 1;
|
int last = embeds.size() - 1;
|
||||||
var lastUrl = pageUrls.get(last);
|
var lastUrl = pageUrls.get(last);
|
||||||
var lastExt = extension(lastUrl);
|
var lastExt = extension(lastUrl);
|
||||||
var lastBuilder = EmbedFactory.create()
|
var lastBuilder =
|
||||||
.withTitle("🔞 " + session.site())
|
EmbedFactory.create()
|
||||||
.withColor(session.color())
|
.withTitle("🔞 " + session.site())
|
||||||
.withFooter(footer);
|
.withColor(session.color())
|
||||||
|
.withFooter(footer);
|
||||||
if (!isVideoExt(lastExt)) {
|
if (!isVideoExt(lastExt)) {
|
||||||
lastBuilder.withImage(lastUrl);
|
lastBuilder.withImage(lastUrl);
|
||||||
}
|
}
|
||||||
|
|
@ -88,8 +88,7 @@ public final class ImageEmbedHelper {
|
||||||
boolean noNext = !session.hasNext() && session.exhausted();
|
boolean noNext = !session.hasNext() && session.exhausted();
|
||||||
return ActionRow.of(
|
return ActionRow.of(
|
||||||
Button.primary(NAV_PREFIX + "prev", "◀ Назад").withDisabled(noPrev),
|
Button.primary(NAV_PREFIX + "prev", "◀ Назад").withDisabled(noPrev),
|
||||||
Button.primary(NAV_PREFIX + "next", "Вперёд ▶").withDisabled(noNext)
|
Button.primary(NAV_PREFIX + "next", "Вперёд ▶").withDisabled(noNext));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String formatTags(String tags) {
|
public static String formatTags(String tags) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ package creator.loki.feature.image;
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import com.google.inject.Provides;
|
import com.google.inject.Provides;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
|
|
||||||
public class ImageModule extends AbstractModule {
|
public class ImageModule extends AbstractModule {
|
||||||
|
|
||||||
|
|
@ -11,6 +13,10 @@ public class ImageModule extends AbstractModule {
|
||||||
protected void configure() {
|
protected void configure() {
|
||||||
bind(ImageSessionManager.class).in(Singleton.class);
|
bind(ImageSessionManager.class).in(Singleton.class);
|
||||||
bind(ImageNavigationHandler.class).in(Singleton.class);
|
bind(ImageNavigationHandler.class).in(Singleton.class);
|
||||||
|
|
||||||
|
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||||
|
.addBinding()
|
||||||
|
.to(ImageCommandRegistrar.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package creator.loki.feature.image;
|
package creator.loki.feature.image;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
||||||
|
|
@ -7,8 +9,6 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class ImageNavigationHandler extends ListenerAdapter {
|
public class ImageNavigationHandler extends ListenerAdapter {
|
||||||
|
|
||||||
|
|
@ -84,7 +84,8 @@ public class ImageNavigationHandler extends ListenerAdapter {
|
||||||
|
|
||||||
private void loadMore(ButtonInteractionEvent event, ImageSession session, String messageId) {
|
private void loadMore(ButtonInteractionEvent event, ImageSession session, String messageId) {
|
||||||
int nextApiPage = session.apiPage() + 1;
|
int nextApiPage = session.apiPage() + 1;
|
||||||
logger.debug("loadMore: site={} tags='{}' apiPage={}", session.site(), session.tags(), nextApiPage);
|
logger.debug(
|
||||||
|
"loadMore: site={} tags='{}' apiPage={}", session.site(), session.tags(), nextApiPage);
|
||||||
|
|
||||||
List<String> newUrls = fetchPage(session.site(), session.tags(), nextApiPage);
|
List<String> newUrls = fetchPage(session.site(), session.tags(), nextApiPage);
|
||||||
logger.debug("loadMore: got {} urls", newUrls.size());
|
logger.debug("loadMore: got {} urls", newUrls.size());
|
||||||
|
|
@ -93,7 +94,8 @@ public class ImageNavigationHandler extends ListenerAdapter {
|
||||||
ImageSession updated = session.withMoreUrls(newUrls);
|
ImageSession updated = session.withMoreUrls(newUrls);
|
||||||
sessionManager.update(messageId, updated);
|
sessionManager.update(messageId, updated);
|
||||||
|
|
||||||
event.getHook()
|
event
|
||||||
|
.getHook()
|
||||||
.editOriginalEmbeds(ImageEmbedHelper.buildEmbeds(updated))
|
.editOriginalEmbeds(ImageEmbedHelper.buildEmbeds(updated))
|
||||||
.setComponents(ImageEmbedHelper.buildActionRow(updated))
|
.setComponents(ImageEmbedHelper.buildActionRow(updated))
|
||||||
.queue();
|
.queue();
|
||||||
|
|
@ -101,18 +103,19 @@ public class ImageNavigationHandler extends ListenerAdapter {
|
||||||
|
|
||||||
private List<String> fetchPage(String site, String tags, int apiPage) {
|
private List<String> fetchPage(String site, String tags, int apiPage) {
|
||||||
return switch (site) {
|
return switch (site) {
|
||||||
case "e621.net" -> imageService.fetchE621(tags, PAGE_FETCH, apiPage);
|
case "e621.net" -> imageService.fetchE621(tags, PAGE_FETCH, apiPage);
|
||||||
case "rule34.xxx" -> imageService.fetchR34(tags, PAGE_FETCH, apiPage);
|
case "rule34.xxx" -> imageService.fetchR34(tags, PAGE_FETCH, apiPage);
|
||||||
case "gelbooru.com" -> imageService.fetchGelbooru(tags, PAGE_FETCH, apiPage);
|
case "gelbooru.com" -> imageService.fetchGelbooru(tags, PAGE_FETCH, apiPage);
|
||||||
case "yande.re" -> imageService.fetchYandere(tags, PAGE_FETCH, apiPage);
|
case "yande.re" -> imageService.fetchYandere(tags, PAGE_FETCH, apiPage);
|
||||||
default -> List.of();
|
default -> List.of();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Вспомогательное ──────────────────────────────────────────────────────────
|
// ── Вспомогательное ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static void editMessage(ButtonInteractionEvent event, ImageSession session) {
|
private static void editMessage(ButtonInteractionEvent event, ImageSession session) {
|
||||||
event.editMessageEmbeds(ImageEmbedHelper.buildEmbeds(session))
|
event
|
||||||
|
.editMessageEmbeds(ImageEmbedHelper.buildEmbeds(session))
|
||||||
.setComponents(ImageEmbedHelper.buildActionRow(session))
|
.setComponents(ImageEmbedHelper.buildActionRow(session))
|
||||||
.queue();
|
.queue();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
package creator.loki.feature.image;
|
package creator.loki.feature.image;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
|
@ -13,11 +18,6 @@ import okhttp3.Response;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class ImageService {
|
public class ImageService {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ImageService.class);
|
private static final Logger logger = LoggerFactory.getLogger(ImageService.class);
|
||||||
|
|
@ -48,11 +48,7 @@ public class ImageService {
|
||||||
int capped = Math.min(limit, 100);
|
int capped = Math.min(limit, 100);
|
||||||
String url = E621_API + "?tags=" + encodeTags(tags) + "&limit=" + capped + "&page=" + page;
|
String url = E621_API + "?tags=" + encodeTags(tags) + "&limit=" + capped + "&page=" + page;
|
||||||
Request request =
|
Request request =
|
||||||
new Request.Builder()
|
new Request.Builder().url(url).header("User-Agent", "LokiBot/1.0 (by loki)").get().build();
|
||||||
.url(url)
|
|
||||||
.header("User-Agent", "LokiBot/1.0 (by loki)")
|
|
||||||
.get()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
try (Response response = client.newCall(request).execute()) {
|
try (Response response = client.newCall(request).execute()) {
|
||||||
if (!response.isSuccessful() || response.body() == null) {
|
if (!response.isSuccessful() || response.body() == null) {
|
||||||
|
|
@ -91,8 +87,7 @@ public class ImageService {
|
||||||
|
|
||||||
public List<String> fetchR34(String tags, int limit, int page) {
|
public List<String> fetchR34(String tags, int limit, int page) {
|
||||||
int capped = Math.min(limit, 100);
|
int capped = Math.min(limit, 100);
|
||||||
String url =
|
String url = R34_API + "&tags=" + encodeTags(tags) + "&limit=" + capped + "&pid=" + (page - 1);
|
||||||
R34_API + "&tags=" + encodeTags(tags) + "&limit=" + capped + "&pid=" + (page - 1);
|
|
||||||
if (!r34ApiKey.isEmpty() && !r34UserId.isEmpty()) {
|
if (!r34ApiKey.isEmpty() && !r34UserId.isEmpty()) {
|
||||||
url += "&api_key=" + r34ApiKey + "&user_id=" + r34UserId;
|
url += "&api_key=" + r34ApiKey + "&user_id=" + r34UserId;
|
||||||
}
|
}
|
||||||
|
|
@ -132,11 +127,8 @@ public class ImageService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> fetchBooruJson(String url, int limit) {
|
private List<String> fetchBooruJson(String url, int limit) {
|
||||||
Request request = new Request.Builder()
|
Request request =
|
||||||
.url(url)
|
new Request.Builder().url(url).header("User-Agent", "LokiBot/1.0 (by loki)").get().build();
|
||||||
.header("User-Agent", "LokiBot/1.0 (by loki)")
|
|
||||||
.get()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
try (Response response = client.newCall(request).execute()) {
|
try (Response response = client.newCall(request).execute()) {
|
||||||
if (!response.isSuccessful() || response.body() == null) {
|
if (!response.isSuccessful() || response.body() == null) {
|
||||||
|
|
@ -145,7 +137,10 @@ public class ImageService {
|
||||||
}
|
}
|
||||||
String body = response.body().string();
|
String body = response.body().string();
|
||||||
if (body.isBlank() || !body.startsWith("[")) {
|
if (body.isBlank() || !body.startsWith("[")) {
|
||||||
logger.warn("Booru API returned non-JSON response (code {}): {}", response.code(), body.length() > 200 ? body.substring(0, 200) : body);
|
logger.warn(
|
||||||
|
"Booru API returned non-JSON response (code {}): {}",
|
||||||
|
response.code(),
|
||||||
|
body.length() > 200 ? body.substring(0, 200) : body);
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
JsonArray posts = JsonParser.parseString(body).getAsJsonArray();
|
JsonArray posts = JsonParser.parseString(body).getAsJsonArray();
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import java.util.List;
|
||||||
/**
|
/**
|
||||||
* Иммутабельная сессия навигации по изображениям.
|
* Иммутабельная сессия навигации по изображениям.
|
||||||
*
|
*
|
||||||
* <p>exhausted = true означает, что API вернул пустой ответ на последнем запросе,
|
* <p>exhausted = true означает, что API вернул пустой ответ на последнем запросе, т.е. новых
|
||||||
* т.е. новых страниц больше нет — тогда кнопка «Вперёд» блокируется.
|
* страниц больше нет — тогда кнопка «Вперёд» блокируется.
|
||||||
*/
|
*/
|
||||||
public record ImageSession(
|
public record ImageSession(
|
||||||
List<String> urls,
|
List<String> urls,
|
||||||
|
|
@ -37,8 +37,17 @@ public record ImageSession(
|
||||||
long userId,
|
long userId,
|
||||||
long guildId) {
|
long guildId) {
|
||||||
return new ImageSession(
|
return new ImageSession(
|
||||||
urls, 0, pageSize, site, color, tags, userId, guildId,
|
urls,
|
||||||
System.currentTimeMillis(), 1, false);
|
0,
|
||||||
|
pageSize,
|
||||||
|
site,
|
||||||
|
color,
|
||||||
|
tags,
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
1,
|
||||||
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Навигация ────────────────────────────────────────────────────────────────
|
// ── Навигация ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -66,25 +75,52 @@ public record ImageSession(
|
||||||
|
|
||||||
public ImageSession withPage(int newPage) {
|
public ImageSession withPage(int newPage) {
|
||||||
return new ImageSession(
|
return new ImageSession(
|
||||||
urls, newPage, pageSize, site, color, tags, userId, guildId,
|
urls,
|
||||||
System.currentTimeMillis(), apiPage, exhausted);
|
newPage,
|
||||||
|
pageSize,
|
||||||
|
site,
|
||||||
|
color,
|
||||||
|
tags,
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
apiPage,
|
||||||
|
exhausted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Добавляем новые URL из следующей API-страницы и переходим на следующую
|
* Добавляем новые URL из следующей API-страницы и переходим на следующую внутреннюю страницу.
|
||||||
* внутреннюю страницу. Если newUrls пустой — помечаем сессию как exhausted.
|
* Если newUrls пустой — помечаем сессию как exhausted.
|
||||||
*/
|
*/
|
||||||
public ImageSession withMoreUrls(List<String> newUrls) {
|
public ImageSession withMoreUrls(List<String> newUrls) {
|
||||||
if (newUrls.isEmpty()) {
|
if (newUrls.isEmpty()) {
|
||||||
// API больше ничего не отдал — остаёмся на месте, ставим флаг
|
// API больше ничего не отдал — остаёмся на месте, ставим флаг
|
||||||
return new ImageSession(
|
return new ImageSession(
|
||||||
urls, page, pageSize, site, color, tags, userId, guildId,
|
urls,
|
||||||
System.currentTimeMillis(), apiPage, true);
|
page,
|
||||||
|
pageSize,
|
||||||
|
site,
|
||||||
|
color,
|
||||||
|
tags,
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
apiPage,
|
||||||
|
true);
|
||||||
}
|
}
|
||||||
List<String> combined = new ArrayList<>(urls);
|
List<String> combined = new ArrayList<>(urls);
|
||||||
combined.addAll(newUrls);
|
combined.addAll(newUrls);
|
||||||
return new ImageSession(
|
return new ImageSession(
|
||||||
combined, page + 1, pageSize, site, color, tags, userId, guildId,
|
combined,
|
||||||
System.currentTimeMillis(), apiPage + 1, false);
|
page + 1,
|
||||||
|
pageSize,
|
||||||
|
site,
|
||||||
|
color,
|
||||||
|
tags,
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
apiPage + 1,
|
||||||
|
false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package creator.loki.feature.image;
|
package creator.loki.feature.image;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package creator.loki.feature.image.commands;
|
package creator.loki.feature.image.commands;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedTemplates;
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
|
@ -9,24 +12,21 @@ import creator.loki.feature.image.ImageSession;
|
||||||
import creator.loki.feature.image.ImageSessionManager;
|
import creator.loki.feature.image.ImageSessionManager;
|
||||||
import net.dv8tion.jda.api.entities.channel.attribute.IAgeRestrictedChannel;
|
import net.dv8tion.jda.api.entities.channel.attribute.IAgeRestrictedChannel;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Общая база для команд поиска изображений.
|
* Общая база для команд поиска изображений. Убирает дублирование: NSFW-проверка, parseArgs,
|
||||||
* Убирает дублирование: NSFW-проверка, parseArgs, buildEmbeds, buildButtons.
|
* buildEmbeds, buildButtons.
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractImageCommand implements Command {
|
public abstract class AbstractImageCommand implements Command {
|
||||||
|
|
||||||
protected static final int PAGE_SIZE = 10;
|
protected static final int PAGE_SIZE = 10;
|
||||||
protected static final int DEFAULT_FETCH = 30;
|
protected static final int DEFAULT_FETCH = 30;
|
||||||
protected static final int MAX_FETCH = 50;
|
protected static final int MAX_FETCH = 50;
|
||||||
|
|
||||||
protected final ImageService imageService;
|
protected final ImageService imageService;
|
||||||
protected final ImageSessionManager sessionManager;
|
protected final ImageSessionManager sessionManager;
|
||||||
|
|
||||||
protected AbstractImageCommand(ImageService imageService, ImageSessionManager sessionManager) {
|
protected AbstractImageCommand(ImageService imageService, ImageSessionManager sessionManager) {
|
||||||
this.imageService = imageService;
|
this.imageService = imageService;
|
||||||
this.sessionManager = sessionManager;
|
this.sessionManager = sessionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,40 +49,54 @@ public abstract class AbstractImageCommand implements Command {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandContext context) {
|
public void execute(CommandContext context) {
|
||||||
if (!isNsfwChannel(context)) {
|
if (!isNsfwChannel(context)) {
|
||||||
context.getChannel()
|
context
|
||||||
.sendMessageEmbeds(EmbedTemplates.error(
|
.getChannel()
|
||||||
"🔞 NSFW", "Эта команда доступна только в каналах с возрастным ограничением."))
|
.sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error(
|
||||||
|
"🔞 NSFW", "Эта команда доступна только в каналах с возрастным ограничением."))
|
||||||
.queue();
|
.queue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ParseResult parsed = parseArgs(context.getArgs());
|
ParseResult parsed = parseArgs(context.getArgs());
|
||||||
String searchingMsg = "Ищу изображения на " + siteDisplayName()
|
String searchingMsg =
|
||||||
+ ImageEmbedHelper.tagsDesc(parsed.tags()) + "...";
|
"Ищу изображения на "
|
||||||
|
+ siteDisplayName()
|
||||||
|
+ ImageEmbedHelper.tagsDesc(parsed.tags())
|
||||||
|
+ "...";
|
||||||
|
|
||||||
context.getChannel()
|
context
|
||||||
|
.getChannel()
|
||||||
.sendMessageEmbeds(EmbedTemplates.info("🔍 Поиск", searchingMsg))
|
.sendMessageEmbeds(EmbedTemplates.info("🔍 Поиск", searchingMsg))
|
||||||
.queue(msg -> {
|
.queue(
|
||||||
List<String> urls = fetchUrls(parsed.tags(), parsed.limit());
|
msg -> {
|
||||||
if (urls.isEmpty()) {
|
List<String> urls = fetchUrls(parsed.tags(), parsed.limit());
|
||||||
msg.editMessageEmbeds(EmbedTemplates.warning(
|
if (urls.isEmpty()) {
|
||||||
"😔 Ничего не найдено",
|
msg.editMessageEmbeds(
|
||||||
"По запросу" + ImageEmbedHelper.tagsDesc(parsed.tags()) + " ничего не найдено."))
|
EmbedTemplates.warning(
|
||||||
.queue();
|
"😔 Ничего не найдено",
|
||||||
return;
|
"По запросу"
|
||||||
}
|
+ ImageEmbedHelper.tagsDesc(parsed.tags())
|
||||||
|
+ " ничего не найдено."))
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ImageSession session = ImageSession.create(
|
ImageSession session =
|
||||||
urls, PAGE_SIZE, siteName(), siteColor(),
|
ImageSession.create(
|
||||||
parsed.tags(),
|
urls,
|
||||||
context.getAuthor().getIdLong(),
|
PAGE_SIZE,
|
||||||
context.getGuild().getIdLong());
|
siteName(),
|
||||||
sessionManager.put(msg.getId(), session);
|
siteColor(),
|
||||||
|
parsed.tags(),
|
||||||
|
context.getAuthor().getIdLong(),
|
||||||
|
context.getGuild().getIdLong());
|
||||||
|
sessionManager.put(msg.getId(), session);
|
||||||
|
|
||||||
msg.editMessageEmbeds(ImageEmbedHelper.buildEmbeds(session))
|
msg.editMessageEmbeds(ImageEmbedHelper.buildEmbeds(session))
|
||||||
.setComponents(ImageEmbedHelper.buildActionRow(session))
|
.setComponents(ImageEmbedHelper.buildActionRow(session))
|
||||||
.queue();
|
.queue();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Вспомогательное ──────────────────────────────────────────────────────────
|
// ── Вспомогательное ──────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,52 @@
|
||||||
package creator.loki.feature.image.commands;
|
package creator.loki.feature.image.commands;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.feature.image.ImageService;
|
import creator.loki.feature.image.ImageService;
|
||||||
import creator.loki.feature.image.ImageSessionManager;
|
import creator.loki.feature.image.ImageSessionManager;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class E621Command extends AbstractImageCommand {
|
public class E621Command extends AbstractImageCommand {
|
||||||
|
|
||||||
public E621Command(ImageService imageService, ImageSessionManager sessionManager) {
|
public E621Command(ImageService imageService, ImageSessionManager sessionManager) {
|
||||||
super(imageService, sessionManager);
|
super(imageService, sessionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public String getName() { return "e621"; }
|
@Override
|
||||||
@Override public String getDescription() { return "Поиск изображений на e621.net"; }
|
public String getName() {
|
||||||
@Override public String getUsage() { return "e621 <кол-во до 50> <теги>"; }
|
return "e621";
|
||||||
@Override public String getCategory() { return CommandCategories.NSFW_IMAGES; }
|
}
|
||||||
|
|
||||||
@Override protected String siteName() { return "e621.net"; }
|
@Override
|
||||||
@Override protected String siteDisplayName() { return "e621"; }
|
public String getDescription() {
|
||||||
@Override protected Color siteColor() { return new Color(102, 45, 145); }
|
return "Поиск изображений на e621.net";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "e621 <кол-во до 50> <теги>";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return CommandCategories.NSFW_IMAGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String siteName() {
|
||||||
|
return "e621.net";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String siteDisplayName() {
|
||||||
|
return "e621";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Color siteColor() {
|
||||||
|
return new Color(102, 45, 145);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<String> fetchUrls(String tags, int limit) {
|
protected List<String> fetchUrls(String tags, int limit) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package creator.loki.feature.image.commands;
|
package creator.loki.feature.image.commands;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedTemplates;
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
|
@ -8,23 +11,46 @@ import creator.loki.feature.image.ImageService;
|
||||||
import creator.loki.feature.image.ImageSession;
|
import creator.loki.feature.image.ImageSession;
|
||||||
import creator.loki.feature.image.ImageSessionManager;
|
import creator.loki.feature.image.ImageSessionManager;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class E621RandomCommand extends AbstractImageCommand {
|
public class E621RandomCommand extends AbstractImageCommand {
|
||||||
|
|
||||||
public E621RandomCommand(ImageService imageService, ImageSessionManager sessionManager) {
|
public E621RandomCommand(ImageService imageService, ImageSessionManager sessionManager) {
|
||||||
super(imageService, sessionManager);
|
super(imageService, sessionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public String getName() { return "e621-random"; }
|
@Override
|
||||||
@Override public String getDescription() { return "Случайные изображения с e621.net"; }
|
public String getName() {
|
||||||
@Override public String getUsage() { return "e621-random <кол-во до 50>"; }
|
return "e621-random";
|
||||||
@Override public String getCategory() { return CommandCategories.NSFW_IMAGES; }
|
}
|
||||||
|
|
||||||
@Override protected String siteName() { return "e621.net"; }
|
@Override
|
||||||
@Override protected String siteDisplayName() { return "e621 (случайные)"; }
|
public String getDescription() {
|
||||||
@Override protected Color siteColor() { return new Color(102, 45, 145); }
|
return "Случайные изображения с e621.net";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "e621-random <кол-во до 50>";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return CommandCategories.NSFW_IMAGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String siteName() {
|
||||||
|
return "e621.net";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String siteDisplayName() {
|
||||||
|
return "e621 (случайные)";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Color siteColor() {
|
||||||
|
return new Color(102, 45, 145);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<String> fetchUrls(String tags, int limit) {
|
protected List<String> fetchUrls(String tags, int limit) {
|
||||||
|
|
@ -35,9 +61,11 @@ public class E621RandomCommand extends AbstractImageCommand {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandContext context) {
|
public void execute(CommandContext context) {
|
||||||
if (!isNsfwChannel(context)) {
|
if (!isNsfwChannel(context)) {
|
||||||
context.getChannel()
|
context
|
||||||
.sendMessageEmbeds(EmbedTemplates.error(
|
.getChannel()
|
||||||
"🔞 NSFW", "Эта команда доступна только в каналах с возрастным ограничением."))
|
.sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error(
|
||||||
|
"🔞 NSFW", "Эта команда доступна только в каналах с возрастным ограничением."))
|
||||||
.queue();
|
.queue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -48,31 +76,40 @@ public class E621RandomCommand extends AbstractImageCommand {
|
||||||
try {
|
try {
|
||||||
limit = Integer.parseInt(args.get(0));
|
limit = Integer.parseInt(args.get(0));
|
||||||
limit = Math.max(1, Math.min(limit, MAX_FETCH));
|
limit = Math.max(1, Math.min(limit, MAX_FETCH));
|
||||||
} catch (NumberFormatException ignored) {}
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final int finalLimit = limit;
|
final int finalLimit = limit;
|
||||||
context.getChannel()
|
context
|
||||||
.sendMessageEmbeds(EmbedTemplates.info("🔍 Случайные", "Ищу случайные изображения на e621..."))
|
.getChannel()
|
||||||
.queue(msg -> {
|
.sendMessageEmbeds(
|
||||||
List<String> urls = imageService.fetchE621("", finalLimit);
|
EmbedTemplates.info("🔍 Случайные", "Ищу случайные изображения на e621..."))
|
||||||
if (urls.isEmpty()) {
|
.queue(
|
||||||
msg.editMessageEmbeds(EmbedTemplates.warning(
|
msg -> {
|
||||||
"😔 Ничего не найдено", "Не удалось найти изображения."))
|
List<String> urls = imageService.fetchE621("", finalLimit);
|
||||||
.queue();
|
if (urls.isEmpty()) {
|
||||||
return;
|
msg.editMessageEmbeds(
|
||||||
}
|
EmbedTemplates.warning(
|
||||||
|
"😔 Ничего не найдено", "Не удалось найти изображения."))
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ImageSession session = ImageSession.create(
|
ImageSession session =
|
||||||
urls, PAGE_SIZE, siteName(), siteColor(),
|
ImageSession.create(
|
||||||
"",
|
urls,
|
||||||
context.getAuthor().getIdLong(),
|
PAGE_SIZE,
|
||||||
context.getGuild().getIdLong());
|
siteName(),
|
||||||
sessionManager.put(msg.getId(), session);
|
siteColor(),
|
||||||
|
"",
|
||||||
|
context.getAuthor().getIdLong(),
|
||||||
|
context.getGuild().getIdLong());
|
||||||
|
sessionManager.put(msg.getId(), session);
|
||||||
|
|
||||||
msg.editMessageEmbeds(ImageEmbedHelper.buildEmbeds(session))
|
msg.editMessageEmbeds(ImageEmbedHelper.buildEmbeds(session))
|
||||||
.setComponents(ImageEmbedHelper.buildActionRow(session))
|
.setComponents(ImageEmbedHelper.buildActionRow(session))
|
||||||
.queue();
|
.queue();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,52 @@
|
||||||
package creator.loki.feature.image.commands;
|
package creator.loki.feature.image.commands;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.feature.image.ImageService;
|
import creator.loki.feature.image.ImageService;
|
||||||
import creator.loki.feature.image.ImageSessionManager;
|
import creator.loki.feature.image.ImageSessionManager;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class GelbooruCommand extends AbstractImageCommand {
|
public class GelbooruCommand extends AbstractImageCommand {
|
||||||
|
|
||||||
public GelbooruCommand(ImageService imageService, ImageSessionManager sessionManager) {
|
public GelbooruCommand(ImageService imageService, ImageSessionManager sessionManager) {
|
||||||
super(imageService, sessionManager);
|
super(imageService, sessionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public String getName() { return "gelbooru"; }
|
@Override
|
||||||
@Override public String getDescription() { return "Поиск изображений на Gelbooru"; }
|
public String getName() {
|
||||||
@Override public String getUsage() { return "gelbooru <кол-во до 50> <теги>"; }
|
return "gelbooru";
|
||||||
@Override public String getCategory() { return CommandCategories.NSFW_IMAGES; }
|
}
|
||||||
|
|
||||||
@Override protected String siteName() { return "gelbooru.com"; }
|
@Override
|
||||||
@Override protected String siteDisplayName() { return "Gelbooru"; }
|
public String getDescription() {
|
||||||
@Override protected Color siteColor() { return new Color(50, 100, 180); }
|
return "Поиск изображений на Gelbooru";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "gelbooru <кол-во до 50> <теги>";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return CommandCategories.NSFW_IMAGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String siteName() {
|
||||||
|
return "gelbooru.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String siteDisplayName() {
|
||||||
|
return "Gelbooru";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Color siteColor() {
|
||||||
|
return new Color(50, 100, 180);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<String> fetchUrls(String tags, int limit) {
|
protected List<String> fetchUrls(String tags, int limit) {
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,52 @@
|
||||||
package creator.loki.feature.image.commands;
|
package creator.loki.feature.image.commands;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.feature.image.ImageService;
|
import creator.loki.feature.image.ImageService;
|
||||||
import creator.loki.feature.image.ImageSessionManager;
|
import creator.loki.feature.image.ImageSessionManager;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class R34Command extends AbstractImageCommand {
|
public class R34Command extends AbstractImageCommand {
|
||||||
|
|
||||||
public R34Command(ImageService imageService, ImageSessionManager sessionManager) {
|
public R34Command(ImageService imageService, ImageSessionManager sessionManager) {
|
||||||
super(imageService, sessionManager);
|
super(imageService, sessionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public String getName() { return "r34"; }
|
@Override
|
||||||
@Override public String getDescription() { return "Поиск изображений на Rule34"; }
|
public String getName() {
|
||||||
@Override public String getUsage() { return "r34 <кол-во до 50> <теги>"; }
|
return "r34";
|
||||||
@Override public String getCategory() { return CommandCategories.NSFW_IMAGES; }
|
}
|
||||||
|
|
||||||
@Override protected String siteName() { return "rule34.xxx"; }
|
@Override
|
||||||
@Override protected String siteDisplayName() { return "Rule34"; }
|
public String getDescription() {
|
||||||
@Override protected Color siteColor() { return new Color(200, 100, 0); }
|
return "Поиск изображений на Rule34";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "r34 <кол-во до 50> <теги>";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return CommandCategories.NSFW_IMAGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String siteName() {
|
||||||
|
return "rule34.xxx";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String siteDisplayName() {
|
||||||
|
return "Rule34";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Color siteColor() {
|
||||||
|
return new Color(200, 100, 0);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<String> fetchUrls(String tags, int limit) {
|
protected List<String> fetchUrls(String tags, int limit) {
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,52 @@
|
||||||
package creator.loki.feature.image.commands;
|
package creator.loki.feature.image.commands;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.feature.image.ImageService;
|
import creator.loki.feature.image.ImageService;
|
||||||
import creator.loki.feature.image.ImageSessionManager;
|
import creator.loki.feature.image.ImageSessionManager;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class YandereCommand extends AbstractImageCommand {
|
public class YandereCommand extends AbstractImageCommand {
|
||||||
|
|
||||||
public YandereCommand(ImageService imageService, ImageSessionManager sessionManager) {
|
public YandereCommand(ImageService imageService, ImageSessionManager sessionManager) {
|
||||||
super(imageService, sessionManager);
|
super(imageService, sessionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public String getName() { return "yandere"; }
|
@Override
|
||||||
@Override public String getDescription() { return "Поиск изображений на yande.re"; }
|
public String getName() {
|
||||||
@Override public String getUsage() { return "yandere <кол-во до 50> <теги>"; }
|
return "yandere";
|
||||||
@Override public String getCategory() { return CommandCategories.NSFW_IMAGES; }
|
}
|
||||||
|
|
||||||
@Override protected String siteName() { return "yande.re"; }
|
@Override
|
||||||
@Override protected String siteDisplayName() { return "yande.re"; }
|
public String getDescription() {
|
||||||
@Override protected Color siteColor() { return new Color(220, 50, 120); }
|
return "Поиск изображений на yande.re";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "yandere <кол-во до 50> <теги>";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return CommandCategories.NSFW_IMAGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String siteName() {
|
||||||
|
return "yande.re";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String siteDisplayName() {
|
||||||
|
return "yande.re";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Color siteColor() {
|
||||||
|
return new Color(220, 50, 120);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<String> fetchUrls(String tags, int limit) {
|
protected List<String> fetchUrls(String tags, int limit) {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,12 @@ package creator.loki.feature.level;
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import com.google.inject.Provides;
|
import com.google.inject.Provides;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
import creator.loki.database.Database;
|
import creator.loki.database.Database;
|
||||||
import creator.loki.feature.config.repository.ConfigRepository;
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
import creator.loki.feature.config.repository.GuildConfigRepository;
|
import creator.loki.feature.config.repository.GuildConfigRepository;
|
||||||
|
import creator.loki.feature.level.commands.LevelCommandRegistrar;
|
||||||
import creator.loki.feature.level.commands.ResetLevelsCommand;
|
import creator.loki.feature.level.commands.ResetLevelsCommand;
|
||||||
import creator.loki.feature.level.domain.XpFormula;
|
import creator.loki.feature.level.domain.XpFormula;
|
||||||
import creator.loki.feature.level.listener.ResetLevelsButtonHandler;
|
import creator.loki.feature.level.listener.ResetLevelsButtonHandler;
|
||||||
|
|
@ -24,6 +27,10 @@ public class LevelModule extends AbstractModule {
|
||||||
protected void configure() {
|
protected void configure() {
|
||||||
bind(VoiceTracker.class).in(Singleton.class);
|
bind(VoiceTracker.class).in(Singleton.class);
|
||||||
bind(ResetLevelsButtonHandler.class).in(Singleton.class);
|
bind(ResetLevelsButtonHandler.class).in(Singleton.class);
|
||||||
|
|
||||||
|
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||||
|
.addBinding()
|
||||||
|
.to(LevelCommandRegistrar.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
import creator.loki.core.Lifecycle;
|
||||||
import creator.loki.feature.economy.service.EconomyService;
|
import creator.loki.feature.economy.service.EconomyService;
|
||||||
import creator.loki.feature.level.domain.LevelUpResult;
|
import creator.loki.feature.level.domain.LevelUpResult;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|
@ -17,7 +18,7 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class VoiceTracker extends ListenerAdapter {
|
public class VoiceTracker extends ListenerAdapter implements Lifecycle {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(VoiceTracker.class);
|
private static final Logger logger = LoggerFactory.getLogger(VoiceTracker.class);
|
||||||
private final LevelService levelService;
|
private final LevelService levelService;
|
||||||
private final EconomyService economyService;
|
private final EconomyService economyService;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.google.inject.Singleton;
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
|
import creator.loki.core.Lifecycle;
|
||||||
import creator.loki.util.LoggerDecorator;
|
import creator.loki.util.LoggerDecorator;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
@ -20,7 +21,7 @@ import org.slf4j.LoggerFactory;
|
||||||
* BotConfig#getHealthHttpPort()} (0 = выкл).
|
* BotConfig#getHealthHttpPort()} (0 = выкл).
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
public class HealthHttpServer {
|
public class HealthHttpServer implements Lifecycle {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(HealthHttpServer.class);
|
private static final Logger logger = LoggerFactory.getLogger(HealthHttpServer.class);
|
||||||
|
|
||||||
private final BotConfig config;
|
private final BotConfig config;
|
||||||
|
|
@ -106,7 +107,8 @@ public class HealthHttpServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void stop() {
|
@Override
|
||||||
|
public synchronized void shutdown() {
|
||||||
if (server != null) {
|
if (server != null) {
|
||||||
server.stop(1);
|
server.stop(1);
|
||||||
server = null;
|
server = null;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package creator.loki.feature.moderation;
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import com.google.inject.Provides;
|
import com.google.inject.Provides;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
import creator.loki.database.Database;
|
import creator.loki.database.Database;
|
||||||
import creator.loki.feature.config.repository.GuildConfigRepository;
|
import creator.loki.feature.config.repository.GuildConfigRepository;
|
||||||
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
|
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
|
||||||
|
|
@ -15,7 +17,11 @@ import creator.loki.feature.moderation.repository.WarningRepository;
|
||||||
public class ModerationModule extends AbstractModule {
|
public class ModerationModule extends AbstractModule {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure() {}
|
protected void configure() {
|
||||||
|
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||||
|
.addBinding()
|
||||||
|
.to(ModerationCommandRegistrar.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import creator.loki.core.Lifecycle;
|
||||||
import creator.loki.feature.config.repository.GuildConfigRepository;
|
import creator.loki.feature.config.repository.GuildConfigRepository;
|
||||||
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
|
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
|
||||||
import creator.loki.feature.moderation.repository.ModerationCaseRepository;
|
import creator.loki.feature.moderation.repository.ModerationCaseRepository;
|
||||||
|
|
@ -17,7 +18,7 @@ import net.dv8tion.jda.api.entities.Role;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class MuteScheduler {
|
public class MuteScheduler implements Lifecycle {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MuteScheduler.class);
|
private static final Logger logger = LoggerFactory.getLogger(MuteScheduler.class);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,13 @@ import creator.loki.feature.moderation.util.ModerationHelper;
|
||||||
import creator.loki.util.MuteDurationParser;
|
import creator.loki.util.MuteDurationParser;
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
import net.dv8tion.jda.api.Permission;
|
import net.dv8tion.jda.api.Permission;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
import net.dv8tion.jda.api.entities.Role;
|
import net.dv8tion.jda.api.entities.Role;
|
||||||
|
|
||||||
public class MuteCommand implements Command {
|
public class MuteCommand implements Command {
|
||||||
|
private static final int MAX_REASON_LENGTH = 500;
|
||||||
|
private static final int MAX_LOG_REASON_LENGTH = 1000;
|
||||||
|
|
||||||
private final ModerationCaseRepository cases;
|
private final ModerationCaseRepository cases;
|
||||||
private final ActiveMuteRepository activeMutes;
|
private final ActiveMuteRepository activeMutes;
|
||||||
private final ConfigRepository configRepo;
|
private final ConfigRepository configRepo;
|
||||||
|
|
@ -73,19 +77,49 @@ public class MuteCommand implements Command {
|
||||||
err(ctx, "Укажи пользователя");
|
err(ctx, "Укажи пользователя");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var t = mentioned.get(0);
|
var target = mentioned.get(0);
|
||||||
if (t.getId().equals(mod.getId())) {
|
if (!validateMute(ctx, target, mod, g)) return;
|
||||||
|
var parsed = parseDurationAndReason(ctx);
|
||||||
|
if (parsed == null) return;
|
||||||
|
Role muteRole;
|
||||||
|
try {
|
||||||
|
muteRole = muteRoleService.getOrCreateMuteRole(g);
|
||||||
|
} catch (Exception e) {
|
||||||
|
err(ctx, "Ошибка роли");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!g.getMember(g.getJDA().getSelfUser()).canInteract(muteRole)) {
|
||||||
|
err(ctx, "Подними роль бота выше роли **Мут**");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long caseId = createMuteCase(g, target, mod, parsed.dur, parsed.reason);
|
||||||
|
if (caseId < 0) {
|
||||||
|
err(ctx, "Ошибка записи кейса");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyMuteRole(ctx, g, target, muteRole, caseId, parsed.dur, parsed.reason, mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateMute(
|
||||||
|
CommandContext ctx, Member target, Member mod, net.dv8tion.jda.api.entities.Guild g) {
|
||||||
|
if (target.getId().equals(mod.getId())) {
|
||||||
err(ctx, "Нельзя замутить себя");
|
err(ctx, "Нельзя замутить себя");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (!mod.canInteract(t)) {
|
if (!mod.canInteract(target)) {
|
||||||
err(ctx, "Роль выше");
|
err(ctx, "Роль выше");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (!g.getMember(g.getJDA().getSelfUser()).canInteract(t)) {
|
if (!g.getMember(g.getJDA().getSelfUser()).canInteract(target)) {
|
||||||
err(ctx, "Бот не может");
|
err(ctx, "Бот не может");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ParsedDuration(MuteDurationParser.MuteDuration dur, String reason) {}
|
||||||
|
|
||||||
|
private ParsedDuration parseDurationAndReason(CommandContext ctx) {
|
||||||
MuteDurationParser.MuteDuration dur = null;
|
MuteDurationParser.MuteDuration dur = null;
|
||||||
String durToken = null;
|
String durToken = null;
|
||||||
for (String a : ctx.getArgs()) {
|
for (String a : ctx.getArgs()) {
|
||||||
|
|
@ -99,7 +133,7 @@ public class MuteCommand implements Command {
|
||||||
}
|
}
|
||||||
if (dur == null) {
|
if (dur == null) {
|
||||||
err(ctx, "Укажи длительность: `30s`, `5m`, `2h`, `1d` или `perm`");
|
err(ctx, "Укажи длительность: `30s`, `5m`, `2h`, `1d` или `perm`");
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
boolean past = false;
|
boolean past = false;
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
|
|
@ -112,85 +146,113 @@ public class MuteCommand implements Command {
|
||||||
if (!sb.isEmpty()) sb.append(' ');
|
if (!sb.isEmpty()) sb.append(' ');
|
||||||
sb.append(a);
|
sb.append(a);
|
||||||
}
|
}
|
||||||
String reasonTmp = sb.toString().trim();
|
String tmp = sb.toString().trim();
|
||||||
String reason = reasonTmp.isEmpty() ? "Без причины" : reasonTmp;
|
String reason = tmp.isEmpty() ? "Без причины" : tmp;
|
||||||
Role muteRole;
|
return new ParsedDuration(dur, reason);
|
||||||
try {
|
}
|
||||||
muteRole = muteRoleService.getOrCreateMuteRole(g);
|
|
||||||
} catch (Exception e) {
|
private long createMuteCase(
|
||||||
err(ctx, "Ошибка роли");
|
net.dv8tion.jda.api.entities.Guild g,
|
||||||
return;
|
Member target,
|
||||||
}
|
Member mod,
|
||||||
if (!g.getMember(g.getJDA().getSelfUser()).canInteract(muteRole)) {
|
MuteDurationParser.MuteDuration dur,
|
||||||
err(ctx, "Подними роль бота выше роли **Мут**");
|
String reason) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
boolean permanent = dur.permanent();
|
boolean permanent = dur.permanent();
|
||||||
long secs = dur.secondsTotal();
|
long secs = dur.secondsTotal();
|
||||||
long untilMs = permanent ? 0L : System.currentTimeMillis() + secs * 1000;
|
long untilMs = permanent ? 0L : System.currentTimeMillis() + secs * 1000;
|
||||||
Long durSec = permanent ? null : secs;
|
Long durSec = permanent ? null : secs;
|
||||||
long caseId =
|
long caseId =
|
||||||
cases.insert(
|
cases.insert(
|
||||||
g.getId(), "MUTE", t.getId(), mod.getId(), reason, durSec, permanent ? null : untilMs);
|
g.getId(),
|
||||||
if (caseId < 0) {
|
"MUTE",
|
||||||
err(ctx, "Ошибка записи кейса");
|
target.getId(),
|
||||||
return;
|
mod.getId(),
|
||||||
|
reason,
|
||||||
|
durSec,
|
||||||
|
permanent ? null : untilMs);
|
||||||
|
if (caseId >= 0) {
|
||||||
|
activeMutes.upsert(g.getId(), target.getId(), permanent ? 0L : untilMs, caseId);
|
||||||
|
muteScheduler.cancelPending(g.getId(), target.getId());
|
||||||
}
|
}
|
||||||
activeMutes.upsert(g.getId(), t.getId(), permanent ? 0L : untilMs, caseId);
|
return caseId;
|
||||||
muteScheduler.cancelPending(g.getId(), t.getId());
|
}
|
||||||
g.addRoleToMember(t, muteRole)
|
|
||||||
|
private void applyMuteRole(
|
||||||
|
CommandContext ctx,
|
||||||
|
net.dv8tion.jda.api.entities.Guild g,
|
||||||
|
Member target,
|
||||||
|
Role muteRole,
|
||||||
|
long caseId,
|
||||||
|
MuteDurationParser.MuteDuration dur,
|
||||||
|
String reason,
|
||||||
|
Member mod) {
|
||||||
|
boolean permanent = dur.permanent();
|
||||||
|
long secs = dur.secondsTotal();
|
||||||
|
long untilMs = permanent ? 0L : System.currentTimeMillis() + secs * 1000;
|
||||||
|
g.addRoleToMember(target, muteRole)
|
||||||
.queue(
|
.queue(
|
||||||
ok -> {
|
ok -> {
|
||||||
muteScheduler.scheduleExpiry(g.getId(), t.getId(), permanent ? 0L : untilMs);
|
muteScheduler.scheduleExpiry(g.getId(), target.getId(), permanent ? 0L : untilMs);
|
||||||
ctx.getChannel()
|
ctx.getChannel()
|
||||||
.sendMessageEmbeds(
|
.sendMessageEmbeds(buildMuteConfirmEmbed(target, caseId, dur, reason, untilMs))
|
||||||
EmbedFactory.create()
|
|
||||||
.withTitle("✅ Мут выдан")
|
|
||||||
.withColor(Color.GREEN)
|
|
||||||
.withDescription(
|
|
||||||
"**Кому:** "
|
|
||||||
+ t.getAsMention()
|
|
||||||
+ "\n**Кейс:** `"
|
|
||||||
+ caseId
|
|
||||||
+ "`"
|
|
||||||
+ (permanent
|
|
||||||
? "\n**Срок:** навсегда"
|
|
||||||
: "\n**Срок:** <t:" + (untilMs / 1000) + ":R>")
|
|
||||||
+ "\n**Причина:** "
|
|
||||||
+ sub(reason, 500))
|
|
||||||
.build())
|
|
||||||
.queue();
|
.queue();
|
||||||
var logId = configRepo.get("moderation.log_channel_id");
|
sendMuteLogEmbed(g, target, mod, caseId, dur, reason, untilMs);
|
||||||
if (logId != null) {
|
|
||||||
var log = g.getTextChannelById(logId);
|
|
||||||
if (log != null) {
|
|
||||||
log.sendMessageEmbeds(
|
|
||||||
new EmbedBuilder()
|
|
||||||
.setTitle("🔇 Мут")
|
|
||||||
.setColor(Color.DARK_GRAY)
|
|
||||||
.addField(
|
|
||||||
"Участник", t.getAsMention() + " (`" + t.getId() + "`)", false)
|
|
||||||
.addField("Модератор", mod.getAsMention(), false)
|
|
||||||
.addField("Кейс", "`" + caseId + "`", true)
|
|
||||||
.addField(
|
|
||||||
"Срок",
|
|
||||||
permanent ? "Навсегда" : "<t:" + (untilMs / 1000) + ":F>",
|
|
||||||
true)
|
|
||||||
.addField("Причина", sub(reason, 1000), false)
|
|
||||||
.setTimestamp(OffsetDateTime.now())
|
|
||||||
.build())
|
|
||||||
.queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
err2 -> {
|
err2 -> {
|
||||||
activeMutes.delete(g.getId(), t.getId());
|
activeMutes.delete(g.getId(), target.getId());
|
||||||
err(ctx, "Ошибка выдачи роли");
|
err(ctx, "Ошибка выдачи роли");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String sub(String s, int max) {
|
private net.dv8tion.jda.api.entities.MessageEmbed buildMuteConfirmEmbed(
|
||||||
return ModerationHelper.trunc(s, max);
|
Member target,
|
||||||
|
long caseId,
|
||||||
|
MuteDurationParser.MuteDuration dur,
|
||||||
|
String reason,
|
||||||
|
long untilMs) {
|
||||||
|
boolean permanent = dur.permanent();
|
||||||
|
return EmbedFactory.create()
|
||||||
|
.withTitle("✅ Мут выдан")
|
||||||
|
.withColor(Color.GREEN)
|
||||||
|
.withDescription(
|
||||||
|
"**Кому:** "
|
||||||
|
+ target.getAsMention()
|
||||||
|
+ "\n**Кейс:** `"
|
||||||
|
+ caseId
|
||||||
|
+ "`"
|
||||||
|
+ (permanent
|
||||||
|
? "\n**Срок:** навсегда"
|
||||||
|
: "\n**Срок:** <t:" + (untilMs / 1000) + ":R>")
|
||||||
|
+ "\n**Причина:** "
|
||||||
|
+ ModerationHelper.trunc(reason, MAX_REASON_LENGTH))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMuteLogEmbed(
|
||||||
|
net.dv8tion.jda.api.entities.Guild g,
|
||||||
|
Member target,
|
||||||
|
Member mod,
|
||||||
|
long caseId,
|
||||||
|
MuteDurationParser.MuteDuration dur,
|
||||||
|
String reason,
|
||||||
|
long untilMs) {
|
||||||
|
var logId = configRepo.get("moderation.log_channel_id");
|
||||||
|
if (logId == null) return;
|
||||||
|
var log = g.getTextChannelById(logId);
|
||||||
|
if (log == null) return;
|
||||||
|
boolean permanent = dur.permanent();
|
||||||
|
log.sendMessageEmbeds(
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle("🔇 Мут")
|
||||||
|
.setColor(Color.DARK_GRAY)
|
||||||
|
.addField("Участник", target.getAsMention() + " (`" + target.getId() + "`)", false)
|
||||||
|
.addField("Модератор", mod.getAsMention(), false)
|
||||||
|
.addField("Кейс", "`" + caseId + "`", true)
|
||||||
|
.addField("Срок", permanent ? "Навсегда" : "<t:" + (untilMs / 1000) + ":F>", true)
|
||||||
|
.addField("Причина", ModerationHelper.trunc(reason, MAX_LOG_REASON_LENGTH), false)
|
||||||
|
.setTimestamp(OffsetDateTime.now())
|
||||||
|
.build())
|
||||||
|
.queue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void err(CommandContext ctx, String msg) {
|
private static void err(CommandContext ctx, String msg) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package creator.loki.feature.music;
|
||||||
|
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
import creator.loki.feature.music.core.LavalinkManager;
|
import creator.loki.feature.music.core.LavalinkManager;
|
||||||
import creator.loki.feature.music.core.PlayerManager;
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
|
|
||||||
|
|
@ -10,5 +12,9 @@ public class MusicModule extends AbstractModule {
|
||||||
protected void configure() {
|
protected void configure() {
|
||||||
bind(LavalinkManager.class).in(Singleton.class);
|
bind(LavalinkManager.class).in(Singleton.class);
|
||||||
bind(PlayerManager.class).in(Singleton.class);
|
bind(PlayerManager.class).in(Singleton.class);
|
||||||
|
|
||||||
|
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||||
|
.addBinding()
|
||||||
|
.to(MusicCommandRegistrar.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,8 @@ public class MusicSlashCommands extends ListenerAdapter {
|
||||||
.addOption(OptionType.USER, "user", "Цель", false),
|
.addOption(OptionType.USER, "user", "Цель", false),
|
||||||
Commands.slash("clear", "Удалить сообщения")
|
Commands.slash("clear", "Удалить сообщения")
|
||||||
.addOption(OptionType.INTEGER, "amount", "Количество (1-100)", true)
|
.addOption(OptionType.INTEGER, "amount", "Количество (1-100)", true)
|
||||||
.addOption(OptionType.USER, "user", "Оставить только сообщения пользователя", false))
|
.addOption(
|
||||||
|
OptionType.USER, "user", "Оставить только сообщения пользователя", false))
|
||||||
.queue();
|
.queue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,9 @@ import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import creator.loki.util.LoggerDecorator;
|
import creator.loki.util.LoggerDecorator;
|
||||||
import dev.arbjerg.lavalink.client.Link;
|
|
||||||
import dev.arbjerg.lavalink.client.player.LavalinkPlayer;
|
import dev.arbjerg.lavalink.client.player.LavalinkPlayer;
|
||||||
import dev.arbjerg.lavalink.client.player.Track;
|
import dev.arbjerg.lavalink.client.player.Track;
|
||||||
import dev.arbjerg.lavalink.protocol.v4.Message.EmittedEvent.TrackEndEvent.AudioTrackEndReason;
|
import dev.arbjerg.lavalink.protocol.v4.Message.EmittedEvent.TrackEndEvent.AudioTrackEndReason;
|
||||||
|
|
@ -20,12 +18,14 @@ public class TrackScheduler {
|
||||||
private static final Logger log = LoggerFactory.getLogger(TrackScheduler.class);
|
private static final Logger log = LoggerFactory.getLogger(TrackScheduler.class);
|
||||||
private static final Consumer<Throwable> LOG_LAV = t -> log.warn("Lav: {}", t.getMessage());
|
private static final Consumer<Throwable> LOG_LAV = t -> log.warn("Lav: {}", t.getMessage());
|
||||||
private final GuildMusicManager manager;
|
private final GuildMusicManager manager;
|
||||||
|
private final TrackStarter trackStarter;
|
||||||
final Queue<Track> queue = new LinkedList<>();
|
final Queue<Track> queue = new LinkedList<>();
|
||||||
private boolean repeating;
|
private boolean repeating;
|
||||||
public final FilterState filterState = new FilterState();
|
public final FilterState filterState = new FilterState();
|
||||||
|
|
||||||
public TrackScheduler(GuildMusicManager manager) {
|
public TrackScheduler(GuildMusicManager manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
|
this.trackStarter = new TrackStarter(manager.getExecutor());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void enqueue(Track track) {
|
public void enqueue(Track track) {
|
||||||
|
|
@ -103,13 +103,11 @@ public class TrackScheduler {
|
||||||
|
|
||||||
public void onTrackEnd(Track lastTrack, AudioTrackEndReason endReason) {
|
public void onTrackEnd(Track lastTrack, AudioTrackEndReason endReason) {
|
||||||
if (endReason == AudioTrackEndReason.LOAD_FAILED) {
|
if (endReason == AudioTrackEndReason.LOAD_FAILED) {
|
||||||
LoggerDecorator.warn(log, "Load failed, skipping to next");
|
handleLoadFailed();
|
||||||
nextOrAutoLeave();
|
|
||||||
} else if (!endReason.getMayStartNext() && endReason != AudioTrackEndReason.FINISHED) {
|
} else if (!endReason.getMayStartNext() && endReason != AudioTrackEndReason.FINISHED) {
|
||||||
LoggerDecorator.warn(log, "Track ended unexpectedly: " + endReason);
|
handleUnexpectedEnd(endReason);
|
||||||
manager.startAutoLeaveTimer();
|
|
||||||
} else if (repeating && endReason == AudioTrackEndReason.FINISHED) {
|
} else if (repeating && endReason == AudioTrackEndReason.FINISHED) {
|
||||||
startTrack(lastTrack.makeClone());
|
handleFinished(lastTrack);
|
||||||
} else {
|
} else {
|
||||||
nextOrAutoLeave();
|
nextOrAutoLeave();
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +116,21 @@ public class TrackScheduler {
|
||||||
void startTrack(Track track) {
|
void startTrack(Track track) {
|
||||||
manager.cancelAutoLeaveTimer();
|
manager.cancelAutoLeaveTimer();
|
||||||
var link = manager.getLavalinkManager().getOrCreateLink(manager.getGuildId());
|
var link = manager.getLavalinkManager().getOrCreateLink(manager.getGuildId());
|
||||||
sendTrackDelayed(link, track, 0);
|
trackStarter.startWithRetry(link, track);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleLoadFailed() {
|
||||||
|
LoggerDecorator.warn(log, "Load failed, skipping to next");
|
||||||
|
nextOrAutoLeave();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUnexpectedEnd(AudioTrackEndReason endReason) {
|
||||||
|
LoggerDecorator.warn(log, "Track ended unexpectedly: " + endReason);
|
||||||
|
manager.startAutoLeaveTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleFinished(Track lastTrack) {
|
||||||
|
startTrack(lastTrack.makeClone());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void nextOrAutoLeave() {
|
private void nextOrAutoLeave() {
|
||||||
|
|
@ -126,21 +138,4 @@ public class TrackScheduler {
|
||||||
if (next != null) startTrack(next);
|
if (next != null) startTrack(next);
|
||||||
else manager.startAutoLeaveTimer();
|
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) {
|
|
||||||
scheduleRetry(() -> sendTrackDelayed(link, track, attempt + 1), 300);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
link.createOrUpdatePlayer()
|
|
||||||
.setTrack(track)
|
|
||||||
.setVolume(100)
|
|
||||||
.subscribe(
|
|
||||||
null, err -> scheduleRetry(() -> sendTrackDelayed(link, track, attempt + 1), 500));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package creator.loki.feature.music.manage;
|
||||||
|
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import dev.arbjerg.lavalink.client.Link;
|
||||||
|
import dev.arbjerg.lavalink.client.player.Track;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
class TrackStarter {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TrackStarter.class);
|
||||||
|
private static final int MAX_ATTEMPTS = 5;
|
||||||
|
private static final long RETRY_DELAY_PLAYER_NULL_MS = 300;
|
||||||
|
private static final long RETRY_DELAY_SUBSCRIBE_ERROR_MS = 500;
|
||||||
|
|
||||||
|
private final ScheduledExecutorService executor;
|
||||||
|
|
||||||
|
TrackStarter(ScheduledExecutorService executor) {
|
||||||
|
this.executor = executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a track on the given link with retry logic (up to {@value MAX_ATTEMPTS} attempts).
|
||||||
|
*
|
||||||
|
* @param link the Lavalink link to play the track on
|
||||||
|
* @param track the track to start
|
||||||
|
*/
|
||||||
|
void startWithRetry(Link link, Track track) {
|
||||||
|
startWithRetry(link, track, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startWithRetry(Link link, Track track, int attempt) {
|
||||||
|
if (attempt > MAX_ATTEMPTS) return;
|
||||||
|
if (link.getCachedPlayer() == null) {
|
||||||
|
executor.schedule(
|
||||||
|
() -> startWithRetry(link, track, attempt + 1),
|
||||||
|
RETRY_DELAY_PLAYER_NULL_MS,
|
||||||
|
TimeUnit.MILLISECONDS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
link.createOrUpdatePlayer()
|
||||||
|
.setTrack(track)
|
||||||
|
.setVolume(100)
|
||||||
|
.subscribe(
|
||||||
|
null,
|
||||||
|
err ->
|
||||||
|
executor.schedule(
|
||||||
|
() -> startWithRetry(link, track, attempt + 1),
|
||||||
|
RETRY_DELAY_SUBSCRIBE_ERROR_MS,
|
||||||
|
TimeUnit.MILLISECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,12 +11,7 @@ import creator.loki.feature.music.core.PlayerManager;
|
||||||
import creator.loki.feature.music.core.SearchManager;
|
import creator.loki.feature.music.core.SearchManager;
|
||||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||||
import creator.loki.util.VoiceHelper;
|
import 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.Track;
|
||||||
import dev.arbjerg.lavalink.client.player.TrackLoaded;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|
||||||
|
|
@ -56,10 +51,7 @@ public class PlayCommand implements Command {
|
||||||
if (guild == null) return;
|
if (guild == null) return;
|
||||||
Member member = ctx.getAuthor();
|
Member member = ctx.getAuthor();
|
||||||
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
||||||
ctx.getChannel()
|
sendErr(ctx, "Зайди в голосовой канал сначала.");
|
||||||
.sendMessageEmbeds(
|
|
||||||
EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала."))
|
|
||||||
.queue();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var args = ctx.getArgs();
|
var args = ctx.getArgs();
|
||||||
|
|
@ -69,27 +61,43 @@ public class PlayCommand implements Command {
|
||||||
}
|
}
|
||||||
String first = args.get(0);
|
String first = args.get(0);
|
||||||
if (SearchHandler.isNumeric(first)) {
|
if (SearchHandler.isNumeric(first)) {
|
||||||
Track t = searchHandler.pickResult(guild.getIdLong(), Integer.parseInt(first) - 1);
|
handlePickResult(ctx, first);
|
||||||
if (t == null) {
|
|
||||||
sendErr(ctx, "Поиск устарел. Сделай новый: `!play <запрос>`");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
playTrack(ctx, t);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!resolveVoiceConnection(ctx)) return;
|
||||||
var lm = playerManager.getLavalinkManager();
|
var lm = playerManager.getLavalinkManager();
|
||||||
if (!guild.getSelfMember().getVoiceState().inAudioChannel()) {
|
|
||||||
var vc = member.getVoiceState().getChannel().asVoiceChannel();
|
|
||||||
guild.getJDA().getDirectAudioController().connect(vc);
|
|
||||||
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());
|
|
||||||
VoiceHelper.waitForCondition(() -> link.getCachedPlayer() != null);
|
|
||||||
if (link.getCachedPlayer() == null) return;
|
|
||||||
}
|
|
||||||
var mm = lm.getMusicManager(guild.getIdLong());
|
var mm = lm.getMusicManager(guild.getIdLong());
|
||||||
|
handlePlayQuery(ctx, args, first, lm, mm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean resolveVoiceConnection(CommandContext ctx) {
|
||||||
|
var guild = ctx.getGuild();
|
||||||
|
var member = ctx.getAuthor();
|
||||||
|
var lm = playerManager.getLavalinkManager();
|
||||||
|
if (!VoiceHelper.connectForPlayback(guild, member, lm)) {
|
||||||
|
sendErr(ctx, "Не удалось подключиться к голосовому каналу.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePickResult(CommandContext ctx, String first) {
|
||||||
|
var guild = ctx.getGuild();
|
||||||
|
Track t = searchHandler.pickResult(guild.getIdLong(), Integer.parseInt(first) - 1);
|
||||||
|
if (t == null) {
|
||||||
|
sendErr(ctx, "Поиск устарел. Сделай новый: `!play <запрос>`");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!resolveVoiceConnection(ctx)) return;
|
||||||
|
enqueueAndNotify(ctx, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePlayQuery(
|
||||||
|
CommandContext ctx,
|
||||||
|
List<String> args,
|
||||||
|
String first,
|
||||||
|
LavalinkManager lm,
|
||||||
|
GuildMusicManager mm) {
|
||||||
boolean isSource = SOURCES.contains(first);
|
boolean isSource = SOURCES.contains(first);
|
||||||
String raw = String.join(" ", isSource ? args.subList(1, args.size()) : args);
|
String raw = String.join(" ", isSource ? args.subList(1, args.size()) : args);
|
||||||
if (raw.isEmpty()) {
|
if (raw.isEmpty()) {
|
||||||
|
|
@ -97,9 +105,9 @@ public class PlayCommand implements Command {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean isUrl = raw.startsWith("http://") || raw.startsWith("https://");
|
boolean isUrl = raw.startsWith("http://") || raw.startsWith("https://");
|
||||||
String query = isUrl ? null : (first.equals("list") ? raw : raw);
|
String query = isUrl ? null : raw;
|
||||||
String id = buildQuery(first, raw);
|
String id = buildQuery(first, raw);
|
||||||
loadAndHandle(ctx, lm, guild.getIdLong(), mm, id, isUrl, query, raw);
|
loadAndHandle(ctx, lm, ctx.getGuild().getIdLong(), mm, id, isUrl, query, raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadAndHandle(
|
private void loadAndHandle(
|
||||||
|
|
@ -111,64 +119,9 @@ public class PlayCommand implements Command {
|
||||||
boolean isUrl,
|
boolean isUrl,
|
||||||
String query,
|
String query,
|
||||||
String raw) {
|
String raw) {
|
||||||
String noMatch =
|
|
||||||
isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw;
|
|
||||||
lm.getOrCreateLink(guildId)
|
lm.getOrCreateLink(guildId)
|
||||||
.loadItem(id)
|
.loadItem(id)
|
||||||
.subscribe(
|
.subscribe(new PlayLoadHandler(ctx, searchHandler, mm, isUrl, query, raw));
|
||||||
new AbstractAudioLoadResultHandler() {
|
|
||||||
@Override
|
|
||||||
public void ontrackLoaded(TrackLoaded r) {
|
|
||||||
if (isUrl) playTrack(ctx, r.getTrack());
|
|
||||||
else showResults(ctx, query, mm, List.of(r.getTrack()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlaylistLoaded(PlaylistLoaded r) {
|
|
||||||
if (isUrl) {
|
|
||||||
int c = 0;
|
|
||||||
for (Track t : r.getTracks()) {
|
|
||||||
mm.getScheduler().enqueue(t);
|
|
||||||
c++;
|
|
||||||
}
|
|
||||||
ctx.getChannel()
|
|
||||||
.sendMessageEmbeds(
|
|
||||||
EmbedTemplates.info(
|
|
||||||
"Плейлист добавлен",
|
|
||||||
"**" + r.getInfo().getName() + "** — " + c + " треков"))
|
|
||||||
.queue();
|
|
||||||
} else if (r.getTracks().size() <= 1 && !r.getTracks().isEmpty()) {
|
|
||||||
playTrack(ctx, r.getTracks().get(0));
|
|
||||||
} else {
|
|
||||||
showResults(ctx, query, mm, r.getTracks());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSearchResultLoaded(SearchResult r) {
|
|
||||||
if (r.getTracks().isEmpty()) {
|
|
||||||
sendErr(ctx, noMatch);
|
|
||||||
} else if (isUrl) {
|
|
||||||
playTrack(ctx, r.getTracks().get(0));
|
|
||||||
} else {
|
|
||||||
showResults(ctx, query, mm, r.getTracks());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void noMatches() {
|
|
||||||
sendErr(ctx, noMatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadFailed(LoadFailed e) {
|
|
||||||
sendErr(
|
|
||||||
ctx,
|
|
||||||
e.getException().getMessage() != null
|
|
||||||
? e.getException().getMessage()
|
|
||||||
: "Неизвестная ошибка");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String buildQuery(String first, String raw) {
|
private static String buildQuery(String first, String raw) {
|
||||||
|
|
@ -181,13 +134,7 @@ public class PlayCommand implements Command {
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showResults(CommandContext ctx, String q, GuildMusicManager m, List<Track> list) {
|
private void enqueueAndNotify(CommandContext ctx, Track track) {
|
||||||
if (q == null) return;
|
|
||||||
searchHandler.storeResults(ctx.getGuild().getIdLong(), list);
|
|
||||||
ctx.getChannel().sendMessageEmbeds(searchHandler.buildResultsEmbed(q, list).build()).queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void playTrack(CommandContext ctx, Track track) {
|
|
||||||
var m = playerManager.getLavalinkManager().getMusicManager(ctx.getGuild().getIdLong());
|
var m = playerManager.getLavalinkManager().getMusicManager(ctx.getGuild().getIdLong());
|
||||||
m.getScheduler().enqueue(track);
|
m.getScheduler().enqueue(track);
|
||||||
ctx.getChannel()
|
ctx.getChannel()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
package creator.loki.feature.music.play;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||||
|
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;
|
||||||
|
|
||||||
|
class PlayLoadHandler extends AbstractAudioLoadResultHandler {
|
||||||
|
private final CommandContext ctx;
|
||||||
|
private final SearchHandler searchHandler;
|
||||||
|
private final GuildMusicManager mm;
|
||||||
|
private final boolean isUrl;
|
||||||
|
private final String query;
|
||||||
|
private final String raw;
|
||||||
|
|
||||||
|
PlayLoadHandler(
|
||||||
|
CommandContext ctx,
|
||||||
|
SearchHandler searchHandler,
|
||||||
|
GuildMusicManager mm,
|
||||||
|
boolean isUrl,
|
||||||
|
String query,
|
||||||
|
String raw) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.searchHandler = searchHandler;
|
||||||
|
this.mm = mm;
|
||||||
|
this.isUrl = isUrl;
|
||||||
|
this.query = query;
|
||||||
|
this.raw = raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void ontrackLoaded(TrackLoaded r) {
|
||||||
|
if (isUrl) enqueueAndNotify(r.getTrack());
|
||||||
|
else showResults(List.of(r.getTrack()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaylistLoaded(PlaylistLoaded r) {
|
||||||
|
if (isUrl) {
|
||||||
|
int count = 0;
|
||||||
|
for (Track t : r.getTracks()) {
|
||||||
|
mm.getScheduler().enqueue(t);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
ctx.getChannel()
|
||||||
|
.sendMessageEmbeds(
|
||||||
|
EmbedTemplates.info(
|
||||||
|
"Плейлист добавлен", "**" + r.getInfo().getName() + "** — " + count + " треков"))
|
||||||
|
.queue();
|
||||||
|
} else if (r.getTracks().size() <= 1 && !r.getTracks().isEmpty()) {
|
||||||
|
enqueueAndNotify(r.getTracks().get(0));
|
||||||
|
} else {
|
||||||
|
showResults(r.getTracks());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSearchResultLoaded(SearchResult r) {
|
||||||
|
String noMatch =
|
||||||
|
isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw;
|
||||||
|
if (r.getTracks().isEmpty()) {
|
||||||
|
sendErr(noMatch);
|
||||||
|
} else if (isUrl) {
|
||||||
|
enqueueAndNotify(r.getTracks().get(0));
|
||||||
|
} else {
|
||||||
|
showResults(r.getTracks());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void noMatches() {
|
||||||
|
String noMatch =
|
||||||
|
isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw;
|
||||||
|
sendErr(noMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadFailed(LoadFailed e) {
|
||||||
|
sendErr(
|
||||||
|
e.getException().getMessage() != null
|
||||||
|
? e.getException().getMessage()
|
||||||
|
: "Неизвестная ошибка");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enqueueAndNotify(Track track) {
|
||||||
|
mm.getScheduler().enqueue(track);
|
||||||
|
ctx.getChannel()
|
||||||
|
.sendMessageEmbeds(
|
||||||
|
EmbedTemplates.info(
|
||||||
|
"Добавлено в очередь",
|
||||||
|
"**"
|
||||||
|
+ track.getInfo().getTitle()
|
||||||
|
+ "**\n"
|
||||||
|
+ SearchHandler.formatDuration(track.getInfo().getLength())))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showResults(List<Track> list) {
|
||||||
|
if (query == null) return;
|
||||||
|
searchHandler.storeResults(ctx.getGuild().getIdLong(), list);
|
||||||
|
ctx.getChannel()
|
||||||
|
.sendMessageEmbeds(searchHandler.buildResultsEmbed(query, list).build())
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendErr(String msg) {
|
||||||
|
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,10 +2,16 @@ package creator.loki.feature.radio;
|
||||||
|
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
|
|
||||||
public class RadioModule extends AbstractModule {
|
public class RadioModule extends AbstractModule {
|
||||||
@Override
|
@Override
|
||||||
protected void configure() {
|
protected void configure() {
|
||||||
bind(RadioManager.class).in(Singleton.class);
|
bind(RadioManager.class).in(Singleton.class);
|
||||||
|
|
||||||
|
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||||
|
.addBinding()
|
||||||
|
.to(RadioCommandRegistrar.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ public class IqCommand implements Command {
|
||||||
iq = ratingService.getDeterministicValue(target.getId(), 123, 0, 140);
|
iq = ratingService.getDeterministicValue(target.getId(), 123, 0, 140);
|
||||||
}
|
}
|
||||||
|
|
||||||
RatingLevel level = RatingLevel.findLevel(iq, RatingConfig.getIqLevels());
|
RatingLevel level = RatingConfig.findLevel(iq, "iq");
|
||||||
context
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
.sendMessageEmbeds(
|
.sendMessageEmbeds(
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ public class MustacheCommand implements Command {
|
||||||
for (int i = 0; i < filled; i++) bar.append("🧔");
|
for (int i = 0; i < filled; i++) bar.append("🧔");
|
||||||
for (int i = filled; i < 10; i++) bar.append("⬜");
|
for (int i = filled; i < 10; i++) bar.append("⬜");
|
||||||
|
|
||||||
RatingLevel level = RatingLevel.findLevel(mustacheLevel, RatingConfig.getMustacheLevels());
|
RatingLevel level = RatingConfig.findLevel(mustacheLevel, "mustache");
|
||||||
context
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
.sendMessageEmbeds(
|
.sendMessageEmbeds(
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ public class PpCommand implements Command {
|
||||||
for (int i = 0; i < size; i++) ppVisual.append("=");
|
for (int i = 0; i < size; i++) ppVisual.append("=");
|
||||||
ppVisual.append("D");
|
ppVisual.append("D");
|
||||||
|
|
||||||
RatingLevel level = RatingLevel.findLevel(size, RatingConfig.getPpLevels());
|
RatingLevel level = RatingConfig.findLevel(size, "pp");
|
||||||
context
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
.sendMessageEmbeds(
|
.sendMessageEmbeds(
|
||||||
|
|
|
||||||
|
|
@ -104,43 +104,59 @@ public class RatingConfig {
|
||||||
LEVELS = Collections.unmodifiableMap(m);
|
LEVELS = Collections.unmodifiableMap(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Data-driven accessor — единая точка доступа для всех рейтингов. */
|
||||||
|
public static List<RatingLevel> getLevels(String ratingName) {
|
||||||
|
var levels = LEVELS.get(ratingName);
|
||||||
|
if (levels == null) {
|
||||||
|
throw new IllegalArgumentException("Unknown rating: " + ratingName);
|
||||||
|
}
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data-driven поиск уровня: `findLevel(value, "iq")` вместо `findLevel(value, getIqLevels())`.
|
||||||
|
*/
|
||||||
|
public static RatingLevel findLevel(int value, String ratingName) {
|
||||||
|
return RatingLevel.findLevel(value, getLevels(ratingName));
|
||||||
|
}
|
||||||
|
|
||||||
public static List<RatingLevel> getMustacheLevels() {
|
public static List<RatingLevel> getMustacheLevels() {
|
||||||
return LEVELS.get("mustache");
|
return getLevels("mustache");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<RatingLevel> getIqLevels() {
|
public static List<RatingLevel> getIqLevels() {
|
||||||
return LEVELS.get("iq");
|
return getLevels("iq");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<RatingLevel> getFatLevels() {
|
public static List<RatingLevel> getFatLevels() {
|
||||||
return LEVELS.get("fat");
|
return getLevels("fat");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<RatingLevel> getPpLevels() {
|
public static List<RatingLevel> getPpLevels() {
|
||||||
return LEVELS.get("pp");
|
return getLevels("pp");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<RatingLevel> getBoykisserLevels() {
|
public static List<RatingLevel> getBoykisserLevels() {
|
||||||
return LEVELS.get("boykisser");
|
return getLevels("boykisser");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<RatingLevel> getRizzLevels() {
|
public static List<RatingLevel> getRizzLevels() {
|
||||||
return LEVELS.get("rizz");
|
return getLevels("rizz");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<RatingLevel> getLuckLevels() {
|
public static List<RatingLevel> getLuckLevels() {
|
||||||
return LEVELS.get("luck");
|
return getLevels("luck");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<RatingLevel> getSimpLevels() {
|
public static List<RatingLevel> getSimpLevels() {
|
||||||
return LEVELS.get("simp");
|
return getLevels("simp");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<RatingLevel> getVibeLevels() {
|
public static List<RatingLevel> getVibeLevels() {
|
||||||
return LEVELS.get("vibe");
|
return getLevels("vibe");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<RatingLevel> getGlazeLevels() {
|
public static List<RatingLevel> getGlazeLevels() {
|
||||||
return LEVELS.get("glaze");
|
return getLevels("glaze");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@ public class AiSessionManager {
|
||||||
|
|
||||||
public void addMessage(
|
public void addMessage(
|
||||||
String channelId, String authorName, String authorId, String content, long timestamp) {
|
String channelId, String authorName, String authorId, String content, long timestamp) {
|
||||||
AiSession session =
|
AiSession session = sessions.computeIfAbsent(channelId, k -> new AiSession());
|
||||||
sessions.computeIfAbsent(channelId, k -> new AiSession());
|
|
||||||
session.addMessage(authorName, authorId, content, timestamp);
|
session.addMessage(authorName, authorId, content, timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,19 +74,13 @@ public class AiSessionManager {
|
||||||
for (int i = start; i < messages.size(); i++) {
|
for (int i = start; i < messages.size(); i++) {
|
||||||
SessionMessage m = messages.get(i);
|
SessionMessage m = messages.get(i);
|
||||||
String label = (i == messages.size() - 1) ? "[now]" : "[history]";
|
String label = (i == messages.size() - 1) ? "[now]" : "[history]";
|
||||||
String formatted =
|
String formatted = label + " " + m.authorName + "(" + m.authorId + "): " + m.content;
|
||||||
label
|
|
||||||
+ " "
|
|
||||||
+ m.authorName
|
|
||||||
+ "("
|
|
||||||
+ m.authorId
|
|
||||||
+ "): "
|
|
||||||
+ m.content;
|
|
||||||
result.add(new CachedMessage(formatted, "", m.timestamp));
|
result.add(new CachedMessage(formatted, "", m.timestamp));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record SessionMessage(String authorName, String authorId, String content, long timestamp) {}
|
private record SessionMessage(
|
||||||
|
String authorName, String authorId, String content, long timestamp) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,13 @@ public class AndrertListener extends ListenerAdapter {
|
||||||
private final String allowedChannelId;
|
private final String allowedChannelId;
|
||||||
private final ConcurrentHashMap<String, Long> trackedUsers = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, Long> trackedUsers = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<String, Long> lastAiReplyTime = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, Long> lastAiReplyTime = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<String, ScheduledFuture<?>> pendingPings = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, ScheduledFuture<?>> pendingPings =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public AndrertListener(RoastService roastService, AiSessionManager sessionManager, BotConfig config) {
|
public AndrertListener(
|
||||||
|
RoastService roastService, AiSessionManager sessionManager, BotConfig config) {
|
||||||
this.roastService = roastService;
|
this.roastService = roastService;
|
||||||
this.sessionManager = sessionManager;
|
this.sessionManager = sessionManager;
|
||||||
this.allowedChannelId = config.getAiChannelId();
|
this.allowedChannelId = config.getAiChannelId();
|
||||||
|
|
@ -102,7 +104,8 @@ public class AndrertListener extends ListenerAdapter {
|
||||||
System.currentTimeMillis());
|
System.currentTimeMillis());
|
||||||
|
|
||||||
String targetName = event.getAuthor().getEffectiveName();
|
String targetName = event.getAuthor().getEffectiveName();
|
||||||
List<CachedMessage> context = sessionManager.getContext(event.getChannel().getId(), CHANNEL_CONTEXT);
|
List<CachedMessage> context =
|
||||||
|
sessionManager.getContext(event.getChannel().getId(), CHANNEL_CONTEXT);
|
||||||
|
|
||||||
String response = roastService.generateResponse(context, targetName, userId);
|
String response = roastService.generateResponse(context, targetName, userId);
|
||||||
if (response == null || response.isBlank()) return;
|
if (response == null || response.isBlank()) return;
|
||||||
|
|
@ -145,12 +148,13 @@ public class AndrertListener extends ListenerAdapter {
|
||||||
String[] lines = {
|
String[] lines = {
|
||||||
"эй " + targetMention + ", ты чё замолчал? я не закончил",
|
"эй " + targetMention + ", ты чё замолчал? я не закончил",
|
||||||
"куда пропал " + targetMention + "? аргументы закончились?",
|
"куда пропал " + targetMention + "? аргументы закончились?",
|
||||||
"ну " + targetMention + ", я жду. или скажешь что тебе нечего ответить?",
|
"ну "
|
||||||
|
+ targetMention
|
||||||
|
+ ", я жду. или скажешь что тебе нечего ответить?",
|
||||||
"тишина в эфире " + targetMention + ". всё, признал поражение?",
|
"тишина в эфире " + targetMention + ". всё, признал поражение?",
|
||||||
targetMention + " слился. ожидаемо",
|
targetMention + " слился. ожидаемо",
|
||||||
};
|
};
|
||||||
String msgText =
|
String msgText = lines[(int) (System.currentTimeMillis() % lines.length)];
|
||||||
lines[(int) (System.currentTimeMillis() % lines.length)];
|
|
||||||
|
|
||||||
channel
|
channel
|
||||||
.getHistory()
|
.getHistory()
|
||||||
|
|
@ -163,11 +167,12 @@ public class AndrertListener extends ListenerAdapter {
|
||||||
if (m.getAuthor().isBot()) continue;
|
if (m.getAuthor().isBot()) continue;
|
||||||
String a = m.getAuthor().getEffectiveName();
|
String a = m.getAuthor().getEffectiveName();
|
||||||
String c =
|
String c =
|
||||||
m.getContentRaw()
|
m.getContentRaw() + replyInfo(m) + attachmentInfo(m);
|
||||||
+ replyInfo(m)
|
|
||||||
+ attachmentInfo(m);
|
|
||||||
sessionManager.addMessage(
|
sessionManager.addMessage(
|
||||||
channelId, a, m.getAuthor().getId(), c,
|
channelId,
|
||||||
|
a,
|
||||||
|
m.getAuthor().getId(),
|
||||||
|
c,
|
||||||
m.getTimeCreated().toEpochSecond() * 1000);
|
m.getTimeCreated().toEpochSecond() * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,10 @@ public class DebateChannelListener extends ListenerAdapter {
|
||||||
if (prev != null) return;
|
if (prev != null) return;
|
||||||
seenMessages.entrySet().removeIf(e -> now - e.getValue() > 5000);
|
seenMessages.entrySet().removeIf(e -> now - e.getValue() > 5000);
|
||||||
|
|
||||||
logger.info("debate msg ch={} a={} text={}",
|
logger.info(
|
||||||
event.getChannel().getId(), event.getAuthor().getId(),
|
"debate msg ch={} a={} text={}",
|
||||||
|
event.getChannel().getId(),
|
||||||
|
event.getAuthor().getId(),
|
||||||
event.getMessage().getContentRaw().replace("\n", " "));
|
event.getMessage().getContentRaw().replace("\n", " "));
|
||||||
debateService.onHumanMessage(event);
|
debateService.onHumanMessage(event);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ public class DebateService {
|
||||||
Files.isDirectory(Paths.get("/app/prompts"))
|
Files.isDirectory(Paths.get("/app/prompts"))
|
||||||
? Paths.get("/app/prompts")
|
? Paths.get("/app/prompts")
|
||||||
: Paths.get("data/prompts");
|
: Paths.get("data/prompts");
|
||||||
private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
private static final DateTimeFormatter DT_FMT =
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
private static final long BOT_START_TIME = System.currentTimeMillis();
|
private static final long BOT_START_TIME = System.currentTimeMillis();
|
||||||
private static final int CHANNEL_CONTEXT = 20;
|
private static final int CHANNEL_CONTEXT = 20;
|
||||||
private static final String[] PERSONALITIES = {"arch", "windows", "gentoo"};
|
private static final String[] PERSONALITIES = {"arch", "windows", "gentoo"};
|
||||||
|
|
@ -87,7 +88,8 @@ public class DebateService {
|
||||||
if (debateId == null || debateId.isBlank()) return;
|
if (debateId == null || debateId.isBlank()) return;
|
||||||
if (!event.getChannel().getId().equals(debateId)) return;
|
if (!event.getChannel().getId().equals(debateId)) return;
|
||||||
if (event.getAuthor().isBot()) return;
|
if (event.getAuthor().isBot()) return;
|
||||||
if (event.getMessage().getContentRaw().isBlank() && event.getMessage().getAttachments().isEmpty()) return;
|
if (event.getMessage().getContentRaw().isBlank()
|
||||||
|
&& event.getMessage().getAttachments().isEmpty()) return;
|
||||||
|
|
||||||
TextChannel channel = event.getChannel().asTextChannel();
|
TextChannel channel = event.getChannel().asTextChannel();
|
||||||
ChannelState state = channels.computeIfAbsent(debateId, k -> new ChannelState());
|
ChannelState state = channels.computeIfAbsent(debateId, k -> new ChannelState());
|
||||||
|
|
@ -97,8 +99,7 @@ public class DebateService {
|
||||||
state.roundCount = 0;
|
state.roundCount = 0;
|
||||||
state.lastMessageId = event.getMessage().getId();
|
state.lastMessageId = event.getMessage().getId();
|
||||||
|
|
||||||
state.currentTask = scheduler.schedule(() ->
|
state.currentTask = scheduler.schedule(() -> doRound(channel, state), 0, TimeUnit.SECONDS);
|
||||||
doRound(channel, state), 0, TimeUnit.SECONDS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doRound(TextChannel channel, ChannelState state) {
|
private void doRound(TextChannel channel, ChannelState state) {
|
||||||
|
|
@ -109,43 +110,57 @@ public class DebateService {
|
||||||
state.cancel();
|
state.cancel();
|
||||||
state.roundDone = true;
|
state.roundDone = true;
|
||||||
|
|
||||||
channel.getHistory().retrievePast(CHANNEL_CONTEXT).queue(messages -> {
|
channel
|
||||||
boolean hasNewHuman = messages.stream().anyMatch(m ->
|
.getHistory()
|
||||||
!m.getAuthor().isBot() && m.getTimeCreated().toEpochSecond() * 1000 > state.lastHumanTime);
|
.retrievePast(CHANNEL_CONTEXT)
|
||||||
if (hasNewHuman) {
|
.queue(
|
||||||
state.roundDone = false;
|
messages -> {
|
||||||
state.lastHumanTime = now;
|
boolean hasNewHuman =
|
||||||
state.lastPersonality = null;
|
messages.stream()
|
||||||
state.currentTask = scheduler.schedule(() ->
|
.anyMatch(
|
||||||
doRound(channel, state), 0, TimeUnit.SECONDS);
|
m ->
|
||||||
return;
|
!m.getAuthor().isBot()
|
||||||
}
|
&& m.getTimeCreated().toEpochSecond() * 1000
|
||||||
|
> state.lastHumanTime);
|
||||||
|
if (hasNewHuman) {
|
||||||
|
state.roundDone = false;
|
||||||
|
state.lastHumanTime = now;
|
||||||
|
state.lastPersonality = null;
|
||||||
|
state.currentTask =
|
||||||
|
scheduler.schedule(() -> doRound(channel, state), 0, TimeUnit.SECONDS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<CachedMessage> ctx = buildContext(messages);
|
List<CachedMessage> ctx = buildContext(messages);
|
||||||
|
|
||||||
if (state.roundCount >= MAX_ROUNDS) return;
|
if (state.roundCount >= MAX_ROUNDS) return;
|
||||||
state.roundCount++;
|
state.roundCount++;
|
||||||
|
|
||||||
String personality = pickPersonality(state.lastPersonality);
|
String personality = pickPersonality(state.lastPersonality);
|
||||||
if (personality == null) return;
|
if (personality == null) return;
|
||||||
state.lastPersonality = personality;
|
state.lastPersonality = personality;
|
||||||
|
|
||||||
String response = generateResponse(personality, ctx);
|
String response = generateResponse(personality, ctx);
|
||||||
if (response == null || response.isBlank()) return;
|
if (response == null || response.isBlank()) return;
|
||||||
|
|
||||||
JDA jda = jdaRegistry.getByPersonality(personality);
|
JDA jda = jdaRegistry.getByPersonality(personality);
|
||||||
TextChannel target = jda.getTextChannelById(debateId);
|
TextChannel target = jda.getTextChannelById(debateId);
|
||||||
if (target == null) return;
|
if (target == null) return;
|
||||||
String replyToId = state.lastMessageId;
|
String replyToId = state.lastMessageId;
|
||||||
target.sendMessage(response)
|
target
|
||||||
.setMessageReference(replyToId)
|
.sendMessage(response)
|
||||||
.queue(m -> state.lastMessageId = m.getId());
|
.setMessageReference(replyToId)
|
||||||
|
.queue(m -> state.lastMessageId = m.getId());
|
||||||
|
|
||||||
state.currentTask = scheduler.schedule(() -> {
|
state.currentTask =
|
||||||
state.roundDone = false;
|
scheduler.schedule(
|
||||||
doRound(channel, state);
|
() -> {
|
||||||
}, 0, TimeUnit.SECONDS);
|
state.roundDone = false;
|
||||||
});
|
doRound(channel, state);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
TimeUnit.SECONDS);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private String pickPersonality(String exclude) {
|
private String pickPersonality(String exclude) {
|
||||||
|
|
@ -185,8 +200,16 @@ public class DebateService {
|
||||||
|
|
||||||
long uptimeSec = (System.currentTimeMillis() - BOT_START_TIME) / 1000;
|
long uptimeSec = (System.currentTimeMillis() - BOT_START_TIME) / 1000;
|
||||||
String now = LocalDateTime.now().format(DT_FMT);
|
String now = LocalDateTime.now().format(DT_FMT);
|
||||||
prompt += "\nтекущее время " + now + " uptime бота " + (uptimeSec / 3600) + "ч "
|
prompt +=
|
||||||
+ ((uptimeSec % 3600) / 60) + "м " + (uptimeSec % 60) + "с";
|
"\nтекущее время "
|
||||||
|
+ now
|
||||||
|
+ " uptime бота "
|
||||||
|
+ (uptimeSec / 3600)
|
||||||
|
+ "ч "
|
||||||
|
+ ((uptimeSec % 3600) / 60)
|
||||||
|
+ "м "
|
||||||
|
+ (uptimeSec % 60)
|
||||||
|
+ "с";
|
||||||
|
|
||||||
StringBuilder userContent = new StringBuilder();
|
StringBuilder userContent = new StringBuilder();
|
||||||
for (CachedMessage m : ctx) {
|
for (CachedMessage m : ctx) {
|
||||||
|
|
@ -196,7 +219,8 @@ public class DebateService {
|
||||||
if (userContent.length() > config.getAiContextChars()) break;
|
if (userContent.length() > config.getAiContextChars()) break;
|
||||||
}
|
}
|
||||||
String text = userContent.toString().strip();
|
String text = userContent.toString().strip();
|
||||||
if (text.length() > config.getAiContextChars()) text = text.substring(0, config.getAiContextChars());
|
if (text.length() > config.getAiContextChars())
|
||||||
|
text = text.substring(0, config.getAiContextChars());
|
||||||
|
|
||||||
String wiki = wikipediaService.search(text);
|
String wiki = wikipediaService.search(text);
|
||||||
if (wiki != null) text += "\n\n[википедия]:\n" + wiki;
|
if (wiki != null) text += "\n\n[википедия]:\n" + wiki;
|
||||||
|
|
@ -226,10 +250,11 @@ public class DebateService {
|
||||||
name = msg.getAuthor().getEffectiveName();
|
name = msg.getAuthor().getEffectiveName();
|
||||||
}
|
}
|
||||||
String label = (i == messages.size() - 1) ? "[now]" : "[history]";
|
String label = (i == messages.size() - 1) ? "[now]" : "[history]";
|
||||||
result.add(new CachedMessage(
|
result.add(
|
||||||
label + " " + name + "(" + msg.getAuthor().getId() + "): " + msg.getContentRaw(),
|
new CachedMessage(
|
||||||
msg.getChannel().getName(),
|
label + " " + name + "(" + msg.getAuthor().getId() + "): " + msg.getContentRaw(),
|
||||||
msg.getTimeCreated().toEpochSecond() * 1000));
|
msg.getChannel().getName(),
|
||||||
|
msg.getTimeCreated().toEpochSecond() * 1000));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@ public class HelperListener extends ListenerAdapter {
|
||||||
if (channelId == null || channelId.isBlank()) return;
|
if (channelId == null || channelId.isBlank()) return;
|
||||||
if (!event.getChannel().getId().equals(channelId)) return;
|
if (!event.getChannel().getId().equals(channelId)) return;
|
||||||
if (event.getAuthor().isBot()) return;
|
if (event.getAuthor().isBot()) return;
|
||||||
if (event.getMessage().getContentRaw().isBlank() && event.getMessage().getAttachments().isEmpty()) return;
|
if (event.getMessage().getContentRaw().isBlank()
|
||||||
|
&& event.getMessage().getAttachments().isEmpty()) return;
|
||||||
|
|
||||||
String raw = event.getMessage().getContentRaw();
|
String raw = event.getMessage().getContentRaw();
|
||||||
if (!raw.isBlank() && !LanguageFilter.isAllowed(raw)) return;
|
if (!raw.isBlank() && !LanguageFilter.isAllowed(raw)) return;
|
||||||
|
|
@ -63,38 +64,53 @@ public class HelperListener extends ListenerAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void respond(MessageReceivedEvent event) {
|
private void respond(MessageReceivedEvent event) {
|
||||||
event.getChannel().getHistory().retrievePast(CHANNEL_CONTEXT).queue(messages -> {
|
event
|
||||||
String targetName = event.getAuthor().getEffectiveName();
|
.getChannel()
|
||||||
List<CachedMessage> context = new ArrayList<>();
|
.getHistory()
|
||||||
for (int idx = 0; idx < messages.size(); idx++) {
|
.retrievePast(CHANNEL_CONTEXT)
|
||||||
Message msg = messages.get(idx);
|
.queue(
|
||||||
boolean isSelf = msg.getAuthor().getId().equals(event.getJDA().getSelfUser().getId());
|
messages -> {
|
||||||
String author = isSelf ? "ты" : msg.getAuthor().getEffectiveName();
|
String targetName = event.getAuthor().getEffectiveName();
|
||||||
String authorId = msg.getAuthor().getId();
|
List<CachedMessage> context = new ArrayList<>();
|
||||||
|
for (int idx = 0; idx < messages.size(); idx++) {
|
||||||
|
Message msg = messages.get(idx);
|
||||||
|
boolean isSelf =
|
||||||
|
msg.getAuthor().getId().equals(event.getJDA().getSelfUser().getId());
|
||||||
|
String author = isSelf ? "ты" : msg.getAuthor().getEffectiveName();
|
||||||
|
String authorId = msg.getAuthor().getId();
|
||||||
|
|
||||||
String label = (idx == 0) ? "[now]" : "[history]";
|
String label = (idx == 0) ? "[now]" : "[history]";
|
||||||
StringBuilder line = new StringBuilder();
|
StringBuilder line = new StringBuilder();
|
||||||
line.append(label).append(" ").append(author).append("(").append(authorId).append("): ")
|
line.append(label)
|
||||||
.append(msg.getContentRaw());
|
.append(" ")
|
||||||
line.append(attachmentInfo(msg));
|
.append(author)
|
||||||
|
.append("(")
|
||||||
|
.append(authorId)
|
||||||
|
.append("): ")
|
||||||
|
.append(msg.getContentRaw());
|
||||||
|
line.append(attachmentInfo(msg));
|
||||||
|
|
||||||
context.add(new CachedMessage(
|
context.add(
|
||||||
line.toString(),
|
new CachedMessage(
|
||||||
event.getChannel().getName(),
|
line.toString(),
|
||||||
msg.getTimeCreated().toEpochSecond() * 1000));
|
event.getChannel().getName(),
|
||||||
}
|
msg.getTimeCreated().toEpochSecond() * 1000));
|
||||||
java.util.Collections.reverse(context);
|
}
|
||||||
|
java.util.Collections.reverse(context);
|
||||||
|
|
||||||
String response = roastService.generateResponse(context, targetName, null, helperPrompt);
|
String response =
|
||||||
if (response == null || response.isBlank()) return;
|
roastService.generateResponse(context, targetName, null, helperPrompt);
|
||||||
|
if (response == null || response.isBlank()) return;
|
||||||
|
|
||||||
sendReply(event, response);
|
sendReply(event, response);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendReply(MessageReceivedEvent event, String text) {
|
private void sendReply(MessageReceivedEvent event, String text) {
|
||||||
java.util.regex.Matcher m = java.util.regex.Pattern.compile(
|
java.util.regex.Matcher m =
|
||||||
"\\[file:([^\\]]+)\\](.*?)\\[/file\\]", java.util.regex.Pattern.DOTALL).matcher(text);
|
java.util.regex.Pattern.compile(
|
||||||
|
"\\[file:([^\\]]+)\\](.*?)\\[/file\\]", java.util.regex.Pattern.DOTALL)
|
||||||
|
.matcher(text);
|
||||||
if (!m.find()) {
|
if (!m.find()) {
|
||||||
sendText(event, text);
|
sendText(event, text);
|
||||||
return;
|
return;
|
||||||
|
|
@ -105,16 +121,15 @@ public class HelperListener extends ListenerAdapter {
|
||||||
String filename = m.group(1).strip();
|
String filename = m.group(1).strip();
|
||||||
String content = m.group(2);
|
String content = m.group(2);
|
||||||
m.appendReplacement(remaining, "");
|
m.appendReplacement(remaining, "");
|
||||||
var data = net.dv8tion.jda.api.utils.FileUpload.fromData(
|
var data =
|
||||||
content.getBytes(java.nio.charset.StandardCharsets.UTF_8), filename);
|
net.dv8tion.jda.api.utils.FileUpload.fromData(
|
||||||
|
content.getBytes(java.nio.charset.StandardCharsets.UTF_8), filename);
|
||||||
if (first) {
|
if (first) {
|
||||||
String msg = remaining.toString().strip();
|
String msg = remaining.toString().strip();
|
||||||
event.getMessage().reply(msg == null ? "" : msg)
|
event.getMessage().reply(msg == null ? "" : msg).addFiles(data).queue();
|
||||||
.addFiles(data).queue();
|
|
||||||
first = false;
|
first = false;
|
||||||
} else {
|
} else {
|
||||||
event.getChannel().sendMessage(" ")
|
event.getChannel().sendMessage(" ").addFiles(data).queue();
|
||||||
.addFiles(data).queue();
|
|
||||||
}
|
}
|
||||||
} while (m.find());
|
} while (m.find());
|
||||||
m.appendTail(remaining);
|
m.appendTail(remaining);
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
@ -28,17 +26,14 @@ public class KnowledgeService {
|
||||||
private static final int RESULT_LIMIT = 3;
|
private static final int RESULT_LIMIT = 3;
|
||||||
private static final Set<String> STOP_WORDS =
|
private static final Set<String> STOP_WORDS =
|
||||||
Set.of(
|
Set.of(
|
||||||
"это", "как", "так", "что", "он", "она", "они", "мы", "вы", "ты", "я",
|
"это", "как", "так", "что", "он", "она", "они", "мы", "вы", "ты", "я", "не", "да", "нет",
|
||||||
"не", "да", "нет", "на", "в", "по", "для", "с", "у", "о", "от", "к",
|
"на", "в", "по", "для", "с", "у", "о", "от", "к", "из", "за", "над", "под", "об", "же",
|
||||||
"из", "за", "над", "под", "об", "же", "ли", "бы", "а", "и", "но",
|
"ли", "бы", "а", "и", "но", "или", "то", "все", "его", "ее", "их", "еще", "уже", "когда",
|
||||||
"или", "то", "все", "его", "ее", "их", "еще", "уже", "когда",
|
"the", "a", "an", "is", "are", "was", "were", "be", "been", "have", "has", "had", "do",
|
||||||
"the", "a", "an", "is", "are", "was", "were", "be", "been",
|
"does", "did", "will", "would", "can", "could", "may", "might", "shall", "should", "to",
|
||||||
"have", "has", "had", "do", "does", "did", "will", "would",
|
"of", "in", "for", "with", "on", "at", "by", "from", "as", "into", "it", "its", "this",
|
||||||
"can", "could", "may", "might", "shall", "should", "to", "of",
|
"that", "these", "those", "i", "you", "he", "she", "we", "they", "me", "him", "her", "us",
|
||||||
"in", "for", "with", "on", "at", "by", "from", "as", "into",
|
"them", "and", "but", "or", "not", "no", "if", "so", "about");
|
||||||
"it", "its", "this", "that", "these", "those", "i", "you",
|
|
||||||
"he", "she", "we", "they", "me", "him", "her", "us", "them",
|
|
||||||
"and", "but", "or", "not", "no", "if", "so", "about");
|
|
||||||
|
|
||||||
private final List<KnowledgeFile> files = new ArrayList<>();
|
private final List<KnowledgeFile> files = new ArrayList<>();
|
||||||
|
|
||||||
|
|
@ -105,10 +100,8 @@ public class KnowledgeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] extractKeywords(String text) {
|
private String[] extractKeywords(String text) {
|
||||||
String cleaned = text.toLowerCase()
|
String cleaned =
|
||||||
.replaceAll("[^a-zа-яё0-9\\s]", " ")
|
text.toLowerCase().replaceAll("[^a-zа-яё0-9\\s]", " ").replaceAll("\\s+", " ").strip();
|
||||||
.replaceAll("\\s+", " ")
|
|
||||||
.strip();
|
|
||||||
String[] all = cleaned.split(" ");
|
String[] all = cleaned.split(" ");
|
||||||
return Arrays.stream(all)
|
return Arrays.stream(all)
|
||||||
.filter(w -> w.length() > 2)
|
.filter(w -> w.length() > 2)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ public class RoastService {
|
||||||
Files.isDirectory(Paths.get("/app/prompts"))
|
Files.isDirectory(Paths.get("/app/prompts"))
|
||||||
? Paths.get("/app/prompts")
|
? Paths.get("/app/prompts")
|
||||||
: Paths.get("data/prompts");
|
: Paths.get("data/prompts");
|
||||||
private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
private static final DateTimeFormatter DT_FMT =
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
private static final long BOT_START_TIME = System.currentTimeMillis();
|
private static final long BOT_START_TIME = System.currentTimeMillis();
|
||||||
|
|
||||||
private final BotConfig config;
|
private final BotConfig config;
|
||||||
|
|
@ -67,7 +68,9 @@ public class RoastService {
|
||||||
long mins = (uptimeSec % 3600) / 60;
|
long mins = (uptimeSec % 3600) / 60;
|
||||||
long secs = uptimeSec % 60;
|
long secs = uptimeSec % 60;
|
||||||
String now = LocalDateTime.now().format(DT_FMT);
|
String now = LocalDateTime.now().format(DT_FMT);
|
||||||
return String.format("текущее время %s uptime бота %dч %dм %dс всего команд выполнено %d", now, hours, mins, secs, metricsCollector.getTotalCommands());
|
return String.format(
|
||||||
|
"текущее время %s uptime бота %dч %dм %dс всего команд выполнено %d",
|
||||||
|
now, hours, mins, secs, metricsCollector.getTotalCommands());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String loadPrompt(String filename) {
|
private String loadPrompt(String filename) {
|
||||||
|
|
@ -171,7 +174,8 @@ public class RoastService {
|
||||||
return deepSeekClient.callModel(systemPrompt, text, 0.9);
|
return deepSeekClient.callModel(systemPrompt, text, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String generateResponse(List<CachedMessage> context, String targetName, String userId, String customPrompt) {
|
public String generateResponse(
|
||||||
|
List<CachedMessage> context, String targetName, String userId, String customPrompt) {
|
||||||
String escapedName = escapeFormat(targetName != null ? targetName : "");
|
String escapedName = escapeFormat(targetName != null ? targetName : "");
|
||||||
String systemPrompt = customPrompt.formatted(escapedName, escapedName, escapedName);
|
String systemPrompt = customPrompt.formatted(escapedName, escapedName, escapedName);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,12 @@ public class WebSearchService {
|
||||||
private final OkHttpClient httpClient;
|
private final OkHttpClient httpClient;
|
||||||
|
|
||||||
public WebSearchService() {
|
public WebSearchService() {
|
||||||
this.httpClient = new OkHttpClient.Builder()
|
this.httpClient =
|
||||||
.connectTimeout(8, TimeUnit.SECONDS)
|
new OkHttpClient.Builder()
|
||||||
.readTimeout(12, TimeUnit.SECONDS)
|
.connectTimeout(8, TimeUnit.SECONDS)
|
||||||
.followRedirects(true)
|
.readTimeout(12, TimeUnit.SECONDS)
|
||||||
.build();
|
.followRedirects(true)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String search(String query) {
|
public String search(String query) {
|
||||||
|
|
@ -68,13 +69,14 @@ public class WebSearchService {
|
||||||
private List<SearchResult> searchDdg(String query) {
|
private List<SearchResult> searchDdg(String query) {
|
||||||
List<SearchResult> results = new ArrayList<>();
|
List<SearchResult> results = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
String url = "https://html.duckduckgo.com/html/?q="
|
String url =
|
||||||
+ URLEncoder.encode(query, StandardCharsets.UTF_8);
|
"https://html.duckduckgo.com/html/?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8);
|
||||||
Request req = new Request.Builder()
|
Request req =
|
||||||
.url(url)
|
new Request.Builder()
|
||||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
.url(url)
|
||||||
.get()
|
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||||
.build();
|
.get()
|
||||||
|
.build();
|
||||||
try (Response resp = httpClient.newCall(req).execute()) {
|
try (Response resp = httpClient.newCall(req).execute()) {
|
||||||
if (!resp.isSuccessful() || resp.body() == null) return results;
|
if (!resp.isSuccessful() || resp.body() == null) return results;
|
||||||
String html = resp.body().string();
|
String html = resp.body().string();
|
||||||
|
|
@ -108,24 +110,28 @@ public class WebSearchService {
|
||||||
if (amp > 0) enc = enc.substring(0, amp);
|
if (amp > 0) enc = enc.substring(0, amp);
|
||||||
return URLDecoder.decode(enc, StandardCharsets.UTF_8);
|
return URLDecoder.decode(enc, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
return ddgUrl;
|
return ddgUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String fetchPageText(String url) {
|
private String fetchPageText(String url) {
|
||||||
try {
|
try {
|
||||||
Request req = new Request.Builder()
|
Request req =
|
||||||
.url(url)
|
new Request.Builder()
|
||||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
.url(url)
|
||||||
.header("Accept", "text/html,application/xhtml+xml")
|
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||||
.header("Accept-Language", "en,ru;q=0.9")
|
.header("Accept", "text/html,application/xhtml+xml")
|
||||||
.get()
|
.header("Accept-Language", "en,ru;q=0.9")
|
||||||
.build();
|
.get()
|
||||||
|
.build();
|
||||||
try (Response resp = httpClient.newCall(req).execute()) {
|
try (Response resp = httpClient.newCall(req).execute()) {
|
||||||
if (!resp.isSuccessful() || resp.body() == null) return null;
|
if (!resp.isSuccessful() || resp.body() == null) return null;
|
||||||
String html = resp.body().string();
|
String html = resp.body().string();
|
||||||
Document doc = Jsoup.parse(html);
|
Document doc = Jsoup.parse(html);
|
||||||
Element main = doc.selectFirst("main, article, .post-content, .entry-content, .readme, .markdown-body, #readme, .container-lg");
|
Element main =
|
||||||
|
doc.selectFirst(
|
||||||
|
"main, article, .post-content, .entry-content, .readme, .markdown-body, #readme, .container-lg");
|
||||||
String text;
|
String text;
|
||||||
if (main != null) {
|
if (main != null) {
|
||||||
main.select("script, style, nav, footer, header, .sidebar, aside").remove();
|
main.select("script, style, nav, footer, header, .sidebar, aside").remove();
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,20 @@ import org.slf4j.LoggerFactory;
|
||||||
@Singleton
|
@Singleton
|
||||||
public class WikipediaService {
|
public class WikipediaService {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(WikipediaService.class);
|
private static final Logger logger = LoggerFactory.getLogger(WikipediaService.class);
|
||||||
private static final String SEARCH_URL = "https://ru.wikipedia.org/w/api.php?action=query&list=search&srsearch=%s&format=json&srlimit=3&srprop=snippet";
|
private static final String SEARCH_URL =
|
||||||
private static final String EXTRACT_URL = "https://ru.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&titles=%s&format=json&exlimit=1";
|
"https://ru.wikipedia.org/w/api.php?action=query&list=search&srsearch=%s&format=json&srlimit=3&srprop=snippet";
|
||||||
|
private static final String EXTRACT_URL =
|
||||||
|
"https://ru.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&titles=%s&format=json&exlimit=1";
|
||||||
private static final int MAX_RESULT_CHARS = 1500;
|
private static final int MAX_RESULT_CHARS = 1500;
|
||||||
|
|
||||||
private final OkHttpClient httpClient;
|
private final OkHttpClient httpClient;
|
||||||
|
|
||||||
public WikipediaService() {
|
public WikipediaService() {
|
||||||
this.httpClient = new OkHttpClient.Builder()
|
this.httpClient =
|
||||||
.connectTimeout(10, TimeUnit.SECONDS)
|
new OkHttpClient.Builder()
|
||||||
.readTimeout(10, TimeUnit.SECONDS)
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
.build();
|
.readTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String search(String query) {
|
public String search(String query) {
|
||||||
|
|
@ -45,9 +48,8 @@ public class WikipediaService {
|
||||||
for (int i = 0; i < Math.min(results.size(), 3); i++) {
|
for (int i = 0; i < Math.min(results.size(), 3); i++) {
|
||||||
JsonObject r = results.get(i).getAsJsonObject();
|
JsonObject r = results.get(i).getAsJsonObject();
|
||||||
String title = r.get("title").getAsString();
|
String title = r.get("title").getAsString();
|
||||||
String snippet = r.get("snippet").getAsString()
|
String snippet =
|
||||||
.replaceAll("<[^>]+>", "")
|
r.get("snippet").getAsString().replaceAll("<[^>]+>", "").replaceAll("&[^;]+;", " ");
|
||||||
.replaceAll("&[^;]+;", " ");
|
|
||||||
String pageContent = getExtract(title);
|
String pageContent = getExtract(title);
|
||||||
sb.append("=== ").append(title).append(" ===\n");
|
sb.append("=== ").append(title).append(" ===\n");
|
||||||
if (pageContent != null) {
|
if (pageContent != null) {
|
||||||
|
|
|
||||||
130
src/main/java/creator/loki/feature/tiktok/TikTokApi.java
Normal file
130
src/main/java/creator/loki/feature/tiktok/TikTokApi.java
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
package creator.loki.feature.tiktok;
|
||||||
|
|
||||||
|
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.MediaType;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class TikTokApi {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(TikTokApi.class);
|
||||||
|
private static final String API_URL = "https://www.tikwm.com/api/";
|
||||||
|
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
|
||||||
|
|
||||||
|
private final OkHttpClient client;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TikTokApi() {
|
||||||
|
this.client =
|
||||||
|
new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TikTokResult fetchVideo(String url) throws TikTokApiException {
|
||||||
|
JsonObject requestBody = new JsonObject();
|
||||||
|
requestBody.addProperty("url", url);
|
||||||
|
|
||||||
|
Request request =
|
||||||
|
new Request.Builder()
|
||||||
|
.url(API_URL)
|
||||||
|
.post(RequestBody.create(requestBody.toString(), JSON))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = client.newCall(request).execute()) {
|
||||||
|
ResponseBody body = response.body();
|
||||||
|
if (body == null) {
|
||||||
|
throw new TikTokApiException("Empty response from API");
|
||||||
|
}
|
||||||
|
|
||||||
|
String responseStr = body.string();
|
||||||
|
JsonObject json = JsonParser.parseString(responseStr).getAsJsonObject();
|
||||||
|
|
||||||
|
int code = json.has("code") ? json.get("code").getAsInt() : -1;
|
||||||
|
if (code != 0) {
|
||||||
|
String msg = json.has("msg") ? json.get("msg").getAsString() : "Unknown error";
|
||||||
|
throw new TikTokApiException("API error: " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject data = json.getAsJsonObject("data");
|
||||||
|
if (data == null) {
|
||||||
|
throw new TikTokApiException("No data in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
String videoUrl = data.has("play") ? data.get("play").getAsString() : null;
|
||||||
|
String title = data.has("title") ? data.get("title").getAsString() : "";
|
||||||
|
int duration = data.has("duration") ? data.get("duration").getAsInt() : 0;
|
||||||
|
String cover = data.has("cover") ? data.get("cover").getAsString() : null;
|
||||||
|
|
||||||
|
JsonObject authorObj = data.has("author") ? data.getAsJsonObject("author") : null;
|
||||||
|
String author =
|
||||||
|
authorObj != null && authorObj.has("nickname")
|
||||||
|
? authorObj.get("nickname").getAsString()
|
||||||
|
: "Unknown";
|
||||||
|
|
||||||
|
if (videoUrl == null || videoUrl.isBlank()) {
|
||||||
|
throw new TikTokApiException("No video URL in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TikTokResult(videoUrl, title, author, duration, cover);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Failed to fetch TikTok video", e);
|
||||||
|
throw new TikTokApiException("Network error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TikTokResult {
|
||||||
|
private final String videoUrl;
|
||||||
|
private final String title;
|
||||||
|
private final String author;
|
||||||
|
private final int duration;
|
||||||
|
private final String cover;
|
||||||
|
|
||||||
|
public TikTokResult(String videoUrl, String title, String author, int duration, String cover) {
|
||||||
|
this.videoUrl = videoUrl;
|
||||||
|
this.title = title;
|
||||||
|
this.author = author;
|
||||||
|
this.duration = duration;
|
||||||
|
this.cover = cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVideoUrl() {
|
||||||
|
return videoUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthor() {
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDuration() {
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCover() {
|
||||||
|
return cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TikTokApiException extends Exception {
|
||||||
|
public TikTokApiException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
src/main/java/creator/loki/feature/tiktok/TikTokCommand.java
Normal file
163
src/main/java/creator/loki/feature/tiktok/TikTokCommand.java
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
package creator.loki.feature.tiktok;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||||
|
import net.dv8tion.jda.api.utils.FileUpload;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class TikTokCommand implements Command {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(TikTokCommand.class);
|
||||||
|
private static final long MAX_FILE_SIZE = 8 * 1024 * 1024;
|
||||||
|
|
||||||
|
private final TikTokApi tikTokApi;
|
||||||
|
private final OkHttpClient downloadClient;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TikTokCommand(TikTokApi tikTokApi) {
|
||||||
|
this.tikTokApi = tikTokApi;
|
||||||
|
this.downloadClient =
|
||||||
|
new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "tiktok";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Скачать видео из TikTok";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "tiktok <url>";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Развлечения";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
if (context.getArgs().isEmpty()) {
|
||||||
|
context
|
||||||
|
.getChannel()
|
||||||
|
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Укажи ссылку на TikTok видео"))
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = context.getArgs().get(0);
|
||||||
|
if (!url.contains("tiktok.com")) {
|
||||||
|
context
|
||||||
|
.getChannel()
|
||||||
|
.sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Невалидная ссылка. Укажи ссылку на TikTok видео"))
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context
|
||||||
|
.getChannel()
|
||||||
|
.sendMessage("🔍 Загружаю видео...")
|
||||||
|
.queue(
|
||||||
|
loadingMsg -> {
|
||||||
|
Thread.startVirtualThread(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
TikTokApi.TikTokResult result = tikTokApi.fetchVideo(url);
|
||||||
|
|
||||||
|
byte[] videoBytes = downloadVideo(result.getVideoUrl());
|
||||||
|
|
||||||
|
if (videoBytes.length > MAX_FILE_SIZE) {
|
||||||
|
loadingMsg
|
||||||
|
.editMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Видео слишком большое (> 8MB)"))
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String durationStr = formatDuration(result.getDuration());
|
||||||
|
|
||||||
|
MessageEmbed embed =
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle(truncate(result.getTitle(), 256))
|
||||||
|
.withAuthor("@" + result.getAuthor())
|
||||||
|
.withColor(new Color(0, 0, 0))
|
||||||
|
.addField("Длительность", durationStr, true)
|
||||||
|
.withThumbnail(result.getCover())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
context
|
||||||
|
.getChannel()
|
||||||
|
.sendFiles(FileUpload.fromData(videoBytes, "tiktok.mp4"))
|
||||||
|
.setEmbeds(embed)
|
||||||
|
.queue(
|
||||||
|
success -> loadingMsg.delete().queue(),
|
||||||
|
failure ->
|
||||||
|
loadingMsg
|
||||||
|
.editMessageEmbeds(
|
||||||
|
EmbedTemplates.error(
|
||||||
|
"Ошибка", "Не удалось отправить видео"))
|
||||||
|
.queue());
|
||||||
|
|
||||||
|
} catch (TikTokApi.TikTokApiException e) {
|
||||||
|
loadingMsg
|
||||||
|
.editMessageEmbeds(EmbedTemplates.error("Ошибка", e.getMessage()))
|
||||||
|
.queue();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Failed to download TikTok video", e);
|
||||||
|
loadingMsg
|
||||||
|
.editMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Ошибка загрузки видео"))
|
||||||
|
.queue();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error in TikTokCommand", e);
|
||||||
|
loadingMsg
|
||||||
|
.editMessageEmbeds(EmbedTemplates.error("Ошибка", "Произошла ошибка"))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] downloadVideo(String videoUrl) throws IOException {
|
||||||
|
Request request = new Request.Builder().url(videoUrl).build();
|
||||||
|
try (Response response = downloadClient.newCall(request).execute()) {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
throw new IOException("Download failed: HTTP " + response.code());
|
||||||
|
}
|
||||||
|
InputStream is = response.body().byteStream();
|
||||||
|
return is.readAllBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDuration(int seconds) {
|
||||||
|
int min = seconds / 60;
|
||||||
|
int sec = seconds % 60;
|
||||||
|
return String.format("%d:%02d", min, sec);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncate(String str, int maxLen) {
|
||||||
|
if (str == null) return "";
|
||||||
|
return str.length() > maxLen ? str.substring(0, maxLen - 3) + "..." : str;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/java/creator/loki/feature/tiktok/TikTokModule.java
Normal file
17
src/main/java/creator/loki/feature/tiktok/TikTokModule.java
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package creator.loki.feature.tiktok;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Provides;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
|
public class TikTokModule extends AbstractModule {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
bind(TikTokApi.class).in(Singleton.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
TikTokCommand provideTikTokCommand(TikTokApi tikTokApi) {
|
||||||
|
return new TikTokCommand(tikTokApi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package creator.loki.feature.utility;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
|
|
||||||
|
public class UtilityModule extends AbstractModule {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||||
|
.addBinding()
|
||||||
|
.to(UtilityCommandRegistrar.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,10 @@ package creator.loki.util;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
import creator.loki.feature.music.core.LavalinkManager;
|
||||||
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|
||||||
public final class VoiceHelper {
|
public final class VoiceHelper {
|
||||||
private static final int RETRIES = 10;
|
private static final int RETRIES = 10;
|
||||||
private static final long DELAY_MS = 200;
|
private static final long DELAY_MS = 200;
|
||||||
|
|
@ -36,4 +40,22 @@ public final class VoiceHelper {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подключается к голосовому каналу для воспроизведения.
|
||||||
|
*
|
||||||
|
* @return true если подключение успешно, false если не удалось
|
||||||
|
*/
|
||||||
|
public static boolean connectForPlayback(Guild guild, Member member, LavalinkManager lm) {
|
||||||
|
if (guild.getSelfMember().getVoiceState().inAudioChannel()) return true;
|
||||||
|
var vc = member.getVoiceState().getChannel().asVoiceChannel();
|
||||||
|
guild.getJDA().getDirectAudioController().connect(vc);
|
||||||
|
if (!guild.getAudioManager().isConnected()) guild.getAudioManager().openAudioConnection(vc);
|
||||||
|
var self = guild.getSelfMember();
|
||||||
|
waitForCondition(() -> self.getVoiceState().inAudioChannel());
|
||||||
|
if (!self.getVoiceState().inAudioChannel()) return false;
|
||||||
|
var link = lm.getOrCreateLink(guild.getIdLong());
|
||||||
|
waitForCondition(() -> link.getCachedPlayer() != null);
|
||||||
|
return link.getCachedPlayer() != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@ class BotConfigTest {
|
||||||
30,
|
30,
|
||||||
24,
|
24,
|
||||||
null,
|
null,
|
||||||
lavalink);
|
lavalink,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
assertEquals("token123", config.getMainToken());
|
assertEquals("token123", config.getMainToken());
|
||||||
assertEquals("token123", config.getToken());
|
assertEquals("token123", config.getToken());
|
||||||
assertNull(config.getMusicToken());
|
assertNull(config.getMusicToken());
|
||||||
|
|
@ -45,7 +47,8 @@ class BotConfigTest {
|
||||||
void handlesNullValues() {
|
void handlesNullValues() {
|
||||||
BotConfig config =
|
BotConfig config =
|
||||||
new BotConfig(
|
new BotConfig(
|
||||||
null, null, null, null, null, 0, null, null, null, null, 10, 300, 3000, 30, 24, null, null);
|
null, null, null, null, null, 0, null, null, null, null, 10, 300, 3000, 30, 24, null,
|
||||||
|
null, null, null);
|
||||||
assertNull(config.getMainToken());
|
assertNull(config.getMainToken());
|
||||||
assertNull(config.getMusicToken());
|
assertNull(config.getMusicToken());
|
||||||
assertNull(config.getRadioToken());
|
assertNull(config.getRadioToken());
|
||||||
|
|
@ -62,7 +65,8 @@ class BotConfigTest {
|
||||||
LavalinkConfig lavalink = new LavalinkConfig("", 0, "", false);
|
LavalinkConfig lavalink = new LavalinkConfig("", 0, "", false);
|
||||||
BotConfig config =
|
BotConfig config =
|
||||||
new BotConfig(
|
new BotConfig(
|
||||||
"", "", "", "", "", 8080, "", "", null, null, 10, 300, 3000, 30, 24, null, lavalink);
|
"", "", "", "", "", 8080, "", "", null, null, 10, 300, 3000, 30, 24, null, lavalink,
|
||||||
|
null, null);
|
||||||
assertEquals("", config.getMainToken());
|
assertEquals("", config.getMainToken());
|
||||||
assertEquals("", config.getMusicToken());
|
assertEquals("", config.getMusicToken());
|
||||||
assertEquals("", config.getRadioToken());
|
assertEquals("", config.getRadioToken());
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import creator.loki.feature.roast.AndrertListener;
|
||||||
import creator.loki.feature.roast.RoastService;
|
import creator.loki.feature.roast.RoastService;
|
||||||
import creator.loki.feature.role.CustomRoleManager;
|
import creator.loki.feature.role.CustomRoleManager;
|
||||||
import creator.loki.feature.ship.repository.ShipPairRepository;
|
import creator.loki.feature.ship.repository.ShipPairRepository;
|
||||||
|
import creator.loki.feature.tiktok.TikTokCommand;
|
||||||
import creator.loki.feature.utility.UtilityCommandRegistrar;
|
import creator.loki.feature.utility.UtilityCommandRegistrar;
|
||||||
import creator.loki.feature.voice.service.VoiceChannelManager;
|
import creator.loki.feature.voice.service.VoiceChannelManager;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
|
@ -102,12 +103,19 @@ class SmokeCommandRegistrationTest {
|
||||||
mock(GuildConfigRepository.class));
|
mock(GuildConfigRepository.class));
|
||||||
moderationRegistrar.register(registry);
|
moderationRegistrar.register(registry);
|
||||||
|
|
||||||
|
var tikTokCommand = mock(TikTokCommand.class);
|
||||||
|
lenient().when(tikTokCommand.getName()).thenReturn("tiktok");
|
||||||
|
lenient().when(tikTokCommand.getDescription()).thenReturn("Скачать видео из TikTok");
|
||||||
|
lenient().when(tikTokCommand.getUsage()).thenReturn("tiktok <url>");
|
||||||
|
lenient().when(tikTokCommand.getCategory()).thenReturn("Развлечения");
|
||||||
|
|
||||||
var entertainmentRegistrar =
|
var entertainmentRegistrar =
|
||||||
new EntertainmentCommandRegistrar(
|
new EntertainmentCommandRegistrar(
|
||||||
mock(CooldownManager.class),
|
mock(CooldownManager.class),
|
||||||
mock(RatingService.class),
|
mock(RatingService.class),
|
||||||
mock(ShipPairRepository.class),
|
mock(ShipPairRepository.class),
|
||||||
mock(BotConfig.class));
|
mock(BotConfig.class),
|
||||||
|
tikTokCommand);
|
||||||
entertainmentRegistrar.register(registry);
|
entertainmentRegistrar.register(registry);
|
||||||
|
|
||||||
var utilityRegistrar =
|
var utilityRegistrar =
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,9 @@ class LavalinkManagerTest {
|
||||||
30,
|
30,
|
||||||
24,
|
24,
|
||||||
null,
|
null,
|
||||||
lavalinkConfig);
|
lavalinkConfig,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
|
||||||
var manager = new LavalinkManager(config, queueRepository);
|
var manager = new LavalinkManager(config, queueRepository);
|
||||||
|
|
||||||
|
|
@ -68,7 +70,9 @@ class LavalinkManagerTest {
|
||||||
30,
|
30,
|
||||||
24,
|
24,
|
||||||
null,
|
null,
|
||||||
lavalinkConfig);
|
lavalinkConfig,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
var manager = new LavalinkManager(config, queueRepository);
|
var manager = new LavalinkManager(config, queueRepository);
|
||||||
|
|
||||||
var first = manager.getMusicManager(123L);
|
var first = manager.getMusicManager(123L);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue