Refactor: remove TTS system, slash commands folder, reorganize stats leaderboard
This commit is contained in:
parent
f43db6a527
commit
64a8928d41
29 changed files with 313 additions and 1764 deletions
309
TODO.md
309
TODO.md
|
|
@ -1,6 +1,313 @@
|
||||||
# TODO - План реализации функций
|
# TODO - План реализации функций
|
||||||
|
|
||||||
## 🎯 Основные задачи
|
## Правила разработки
|
||||||
|
|
||||||
|
### Архитектура и код:
|
||||||
|
- KISS, DRY, SOLID принципы
|
||||||
|
- Файлы максимум 150-200 строк, стараться делать 100 строк
|
||||||
|
- В одной папке не больше 6 файлов
|
||||||
|
- Не создавать новые MD файлы без необходимости
|
||||||
|
- Разделение по доменам, а не по типам артефактов
|
||||||
|
|
||||||
|
### Принципы структуры:
|
||||||
|
- Пакет = домен/фича, а не тип файла (не "команды/листенеры", а "profile/level/voice")
|
||||||
|
- Каждая фича содержит свои команды, сервисы, репозитории
|
||||||
|
- Единый слой персистентности через repository/
|
||||||
|
- Интеграции в отдельном пакете integration/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Рефакторинг структуры проекта
|
||||||
|
|
||||||
|
### Целевая оценка: 7/10
|
||||||
|
|
||||||
|
**Проблемы:**
|
||||||
|
1. `util/` - мусорная корзина (CooldownManager, RateLimiter, NekosLifeApi)
|
||||||
|
2. Два механизма персистентности (data/ и database/) без абстракции
|
||||||
|
3. `command/impl/entertainment/` - 23 файла в одном пакете
|
||||||
|
4. `exception/` - избыточный пакет для 2 файлов
|
||||||
|
5. `embed/` - presentation layer оторван от domain
|
||||||
|
6. `listener/` - смешаны разные ответственности
|
||||||
|
7. Техническое разделение вместо доменного
|
||||||
|
8. **LevelSystem - God Class (5/10):**
|
||||||
|
- Нарушение SRP: хранение + кэширование + формулы + лидерборды + прогресс-бары
|
||||||
|
- Прямая зависимость от JsonDataStore (нет интерфейса Repository)
|
||||||
|
- O(n) на каждое сообщение (читает/пишет весь JSON)
|
||||||
|
- Кэш messageTimestamps не персистентный (теряется при рестарте)
|
||||||
|
- Рассогласование формул: комментарий говорит 200, код использует 100
|
||||||
|
- LevelUpResult и LevelProgress - вложенные классы вместо отдельных
|
||||||
|
- getAllStats() возвращает mutable map (можно сломать консистентность)
|
||||||
|
9. **UserStats - анемичная модель (6/10):**
|
||||||
|
- Сеттеры позволяют нарушить инварианты (setLevel(999) без XP)
|
||||||
|
- incrementMessages() скрыто меняет lastMessageTime (side effect)
|
||||||
|
- Поля lastMessageTime и lastVoiceJoin не используются системой
|
||||||
|
- Нет equals/hashCode для использования в коллекциях
|
||||||
|
|
||||||
|
### Целевая структура (по доменам):
|
||||||
|
|
||||||
|
```
|
||||||
|
src/main/java/creator/loki/
|
||||||
|
├── App.java # Точка входа (временно, потом в bootstrap/)
|
||||||
|
│
|
||||||
|
├── core/ # Ядро системы
|
||||||
|
│ ├── BotInitializer
|
||||||
|
│ ├── EventDispatcher
|
||||||
|
│ ├── registry/ # Регистрация команд
|
||||||
|
│ │ ├── CommandRegistry
|
||||||
|
│ │ ├── SlashCommandRegistry
|
||||||
|
│ │ ├── PrefixCommandRegistrar
|
||||||
|
│ │ └── SlashCommandRegistrar
|
||||||
|
│ ├── security/ # Безопасность и ограничения
|
||||||
|
│ │ ├── RateLimiter # (из util/)
|
||||||
|
│ │ └── CooldownManager # (из util/)
|
||||||
|
│ └── exception/ # Глобальная обработка ошибок
|
||||||
|
│ └── CommandExceptionHandler
|
||||||
|
│
|
||||||
|
├── command/ # Базовые интерфейсы команд
|
||||||
|
│ ├── Command.java
|
||||||
|
│ ├── CommandContext.java
|
||||||
|
│ ├── CommandException.java # (из exception/)
|
||||||
|
│ ├── SlashCommand.java
|
||||||
|
│ ├── moderation/ # Модерация (3 файла)
|
||||||
|
│ │ ├── BanCommand
|
||||||
|
│ │ ├── KickCommand
|
||||||
|
│ │ └── ClearCommand
|
||||||
|
│ ├── entertainment/ # Развлечения
|
||||||
|
│ │ ├── games/ # Игры (12 файлов)
|
||||||
|
│ │ └── social/ # Взаимодействия (11 файлов)
|
||||||
|
│ ├── utility/ # Утилиты (5 файлов)
|
||||||
|
│ │ ├── HelpCommand
|
||||||
|
│ │ ├── PingCommand
|
||||||
|
│ │ ├── InfoCommand
|
||||||
|
│ │ ├── AvatarCommand
|
||||||
|
│ │ └── PollCommand
|
||||||
|
│ └── system/ # Системные команды
|
||||||
|
│ └── MetricsCommand # (из utility/)
|
||||||
|
│
|
||||||
|
├── feature/ # Крупные фичи/домены
|
||||||
|
│ ├── level/ # Система уровней
|
||||||
|
│ │ ├── LevelSystem
|
||||||
|
│ │ ├── UserStats
|
||||||
|
│ │ ├── LevelUtils # (из profile/)
|
||||||
|
│ │ ├── RankCommand # (из command/stats/)
|
||||||
|
│ │ ├── StatsCommand # (из command/stats/)
|
||||||
|
│ │ ├── LevelUpNotifier # (из listener/)
|
||||||
|
│ │ └── VoiceTracker # (из listener/)
|
||||||
|
│ │
|
||||||
|
│ ├── profile/ # Профили пользователей
|
||||||
|
│ │ ├── ProfileService
|
||||||
|
│ │ ├── ProfileGenerator
|
||||||
|
│ │ ├── ProfileData
|
||||||
|
│ │ ├── ProfileCommand # (из command/utility/)
|
||||||
|
│ │ └── ProfileEmbedFactory # (из embed/)
|
||||||
|
│ │
|
||||||
|
│ ├── reputation/ # Система репутации
|
||||||
|
│ │ ├── ReputationService # (логика из RepCommand)
|
||||||
|
│ │ ├── RepCommand # (из command/stats/)
|
||||||
|
│ │ ├── MinusRepCommand # (из command/stats/)
|
||||||
|
│ │ └── RepTopCommand # (из command/stats/)
|
||||||
|
│ │
|
||||||
|
│ ├── leaderboard/ # Таблица лидеров
|
||||||
|
│ │ ├── LeaderboardCommand # (NewLeaderboardCommand)
|
||||||
|
│ │ ├── LeaderboardService # (логика из команды)
|
||||||
|
│ │ ├── LeaderboardFormatter
|
||||||
|
│ │ ├── LeaderboardButtons
|
||||||
|
│ │ └── LeaderboardButtonListener
|
||||||
|
│ │
|
||||||
|
│ ├── voice/ # Личные голосовые каналы
|
||||||
|
│ │ ├── VoiceChannelManager
|
||||||
|
│ │ ├── VoiceChannelData
|
||||||
|
│ │ ├── VoicePreset
|
||||||
|
│ │ ├── VoiceSetupCommand # (из command/setup/)
|
||||||
|
│ │ ├── VoiceChannelListener # (из listener/)
|
||||||
|
│ │ └── VoiceButtonHandler # (из listener/)
|
||||||
|
│ │
|
||||||
|
│ ├── counter/ # Счетчик участников
|
||||||
|
│ │ ├── CounterManager
|
||||||
|
│ │ ├── DailyStats
|
||||||
|
│ │ ├── CounterSetupCommand # (из command/setup/)
|
||||||
|
│ │ └── MemberCounterListener # (из listener/)
|
||||||
|
│ │
|
||||||
|
│ └── role/ # Кастомные роли
|
||||||
|
│ ├── CustomRoleManager
|
||||||
|
│ ├── CreateRoleCommand # (из command/roles/)
|
||||||
|
│ ├── DeleteRoleCommand # (из command/roles/)
|
||||||
|
│ ├── EditRoleCommand # (из command/roles/)
|
||||||
|
│ ├── MyRolesCommand # (из command/roles/)
|
||||||
|
│ └── RoleCommandValidator # (из command/roles/)
|
||||||
|
│
|
||||||
|
├── event/ # Обработчики событий Discord
|
||||||
|
│ ├── ReadyListener # (из listener/)
|
||||||
|
│ ├── MessageListener # (из listener/)
|
||||||
|
│ ├── SlashCommandListener # (из listener/)
|
||||||
|
│ ├── HelpMenuHandler # (из listener/)
|
||||||
|
│ └── ModerationLogger # (из listener/)
|
||||||
|
│
|
||||||
|
├── repository/ # Единый слой данных
|
||||||
|
│ ├── UserRepository # Интерфейс
|
||||||
|
│ ├── GuildConfigRepository # Интерфейс
|
||||||
|
│ └── impl/
|
||||||
|
│ ├── JsonUserRepository # (из data/)
|
||||||
|
│ └── SqliteConfigRepository # (из database/)
|
||||||
|
│
|
||||||
|
├── database/ # Инфраструктура БД
|
||||||
|
│ ├── Database
|
||||||
|
│ ├── DatabaseSchema
|
||||||
|
│ └── DatabaseMigration
|
||||||
|
│
|
||||||
|
├── integration/ # Внешние API
|
||||||
|
│ └── nekos/
|
||||||
|
│ └── NekosLifeApi # (из util/)
|
||||||
|
│
|
||||||
|
├── config/ # Конфигурация
|
||||||
|
│ ├── BotConfig
|
||||||
|
│ └── ConfigLoader
|
||||||
|
│
|
||||||
|
├── embed/ # Общие шаблоны embed
|
||||||
|
│ ├── EmbedFactory
|
||||||
|
│ └── EmbedTemplates
|
||||||
|
│
|
||||||
|
├── metrics/ # Метрики
|
||||||
|
│ └── MetricsCollector
|
||||||
|
│
|
||||||
|
└── di/ # Dependency Injection
|
||||||
|
└── BotModule
|
||||||
|
```
|
||||||
|
|
||||||
|
### План действий (приоритеты):
|
||||||
|
|
||||||
|
#### Высокий приоритет (делаем сейчас):
|
||||||
|
|
||||||
|
**1. Удалить ненужное**
|
||||||
|
- [x] Удалить `command/slash/` (дубликаты)
|
||||||
|
- [x] Удалить `audio/` (TTS не работает)
|
||||||
|
- [x] Удалить `TtsCommand.java`
|
||||||
|
- [ ] Удалить `SwapCommand.java`
|
||||||
|
- [ ] Удалить все импорты AudioManager
|
||||||
|
|
||||||
|
**2. Реорганизовать util/ (мусорная корзина)**
|
||||||
|
- [ ] Создать `core/security/`
|
||||||
|
- [ ] Переместить `CooldownManager` → `core/security/`
|
||||||
|
- [ ] Переместить `GlobalRateLimiter` → `core/security/`
|
||||||
|
- [ ] Создать `integration/nekos/`
|
||||||
|
- [ ] Переместить `NekosLifeApi` → `integration/nekos/`
|
||||||
|
- [ ] Удалить пустую папку `util/`
|
||||||
|
|
||||||
|
**3. Создать feature/level/ (система уровней)**
|
||||||
|
- [ ] Создать `feature/level/`
|
||||||
|
- [ ] Создать `feature/level/domain/` для доменных объектов
|
||||||
|
- [ ] Создать `feature/level/service/` для сервисов
|
||||||
|
- [ ] Создать `feature/level/repository/` для интерфейсов
|
||||||
|
- [ ] **Рефакторинг LevelSystem (God Class 5/10):**
|
||||||
|
- [ ] Вынести `LevelUpResult` в `feature/level/domain/LevelUpResult.java`
|
||||||
|
- [ ] Вынести `LevelProgress` в `feature/level/domain/LevelProgress.java`
|
||||||
|
- [ ] Создать `feature/level/domain/XpFormula.java` (инкапсуляция математики)
|
||||||
|
- [ ] Создать `feature/level/repository/UserStatsRepository.java` (интерфейс)
|
||||||
|
- [ ] Создать `feature/level/repository/JsonUserStatsRepository.java` (реализация)
|
||||||
|
- [ ] Создать `feature/level/service/LeaderboardService.java` (топы и агрегации)
|
||||||
|
- [ ] Исправить формулу: 100 vs 200 (рассогласование в коде)
|
||||||
|
- [ ] Убрать `messageTimestamps` ConcurrentHashMap, использовать `UserStats.lastMessageTime`
|
||||||
|
- [ ] Рефакторить `LevelSystem` → `feature/level/service/LevelService.java`
|
||||||
|
- [ ] **Рефакторинг UserStats (анемичная модель 6/10):**
|
||||||
|
- [ ] Сделать immutable (убрать сеттеры)
|
||||||
|
- [ ] Добавить методы `addXP()`, `recordMessage()`, `addVoiceTime()`
|
||||||
|
- [ ] Добавить `getLevel(XpFormula)` вместо внешних вычислений
|
||||||
|
- [ ] Добавить `equals()` и `hashCode()`
|
||||||
|
- [ ] Переименовать `incrementMessages()` → `recordMessage(long timestamp)`
|
||||||
|
- [ ] Переместить `level/LevelSystem` → `feature/level/service/LevelService.java`
|
||||||
|
- [ ] Переместить `level/UserStats` → `feature/level/domain/UserStats.java`
|
||||||
|
- [ ] Переместить `profile/LevelUtils` → `feature/level/` (если нужен)
|
||||||
|
- [ ] Переместить `command/impl/stats/RankCommand` → `feature/level/`
|
||||||
|
- [ ] Переместить `command/impl/stats/StatsCommand` → `feature/level/`
|
||||||
|
- [ ] Переместить `listener/LevelUpNotifier` → `feature/level/`
|
||||||
|
- [ ] Переместить `listener/VoiceTracker` → `feature/level/`
|
||||||
|
- [ ] Удалить пустую папку `level/`
|
||||||
|
|
||||||
|
**4. Создать feature/voice/ (голосовые каналы)**
|
||||||
|
- [ ] Создать `feature/voice/`
|
||||||
|
- [ ] Переместить `voice/*` → `feature/voice/`
|
||||||
|
- [ ] Переместить `command/impl/setup/VoiceSetupCommand` → `feature/voice/`
|
||||||
|
- [ ] Переместить `listener/VoiceChannelListener` → `feature/voice/`
|
||||||
|
- [ ] Переместить `listener/VoiceButtonHandler` → `feature/voice/`
|
||||||
|
- [ ] Удалить пустую папку `voice/`
|
||||||
|
|
||||||
|
**5. Создать feature/counter/ (счетчик)**
|
||||||
|
- [ ] Создать `feature/counter/`
|
||||||
|
- [ ] Переместить `counter/*` → `feature/counter/`
|
||||||
|
- [ ] Переместить `command/impl/setup/CounterSetupCommand` → `feature/counter/`
|
||||||
|
- [ ] Переместить `listener/MemberCounterListener` → `feature/counter/`
|
||||||
|
- [ ] Удалить пустую папку `counter/`
|
||||||
|
|
||||||
|
**6. Создать feature/role/ (кастомные роли)**
|
||||||
|
- [ ] Создать `feature/role/`
|
||||||
|
- [ ] Переместить `role/CustomRoleManager` → `feature/role/`
|
||||||
|
- [ ] Переместить `command/impl/roles/*` → `feature/role/`
|
||||||
|
- [ ] Удалить пустые папки `role/` и `command/impl/roles/`
|
||||||
|
|
||||||
|
#### Средний приоритет (потом):
|
||||||
|
|
||||||
|
**7. Создать feature/reputation/**
|
||||||
|
- [ ] Создать `feature/reputation/`
|
||||||
|
- [ ] Создать `ReputationService` (вынести логику из команд)
|
||||||
|
- [ ] Переместить `command/impl/stats/RepCommand` → `feature/reputation/`
|
||||||
|
- [ ] Переместить `command/impl/stats/MinusRepCommand` → `feature/reputation/`
|
||||||
|
- [ ] Переместить `command/impl/stats/RepTopCommand` → `feature/reputation/`
|
||||||
|
|
||||||
|
**8. Создать feature/leaderboard/**
|
||||||
|
- [ ] Создать `feature/leaderboard/`
|
||||||
|
- [ ] Переместить `command/impl/stats/leaderboard/*` → `feature/leaderboard/`
|
||||||
|
- [ ] Создать `LeaderboardService`
|
||||||
|
- [ ] Удалить пустую папку `command/impl/stats/`
|
||||||
|
|
||||||
|
**9. Реорганизовать profile/**
|
||||||
|
- [ ] Создать `feature/profile/`
|
||||||
|
- [ ] Переместить `profile/*` → `feature/profile/`
|
||||||
|
- [ ] Переместить `command/impl/utility/ProfileCommand` → `feature/profile/`
|
||||||
|
- [ ] Создать `ProfileEmbedFactory` (вынести из команды)
|
||||||
|
- [ ] Удалить пустую папку `profile/`
|
||||||
|
|
||||||
|
**10. Реорганизовать event/ (listener)**
|
||||||
|
- [ ] Создать `event/`
|
||||||
|
- [ ] Переместить `listener/ReadyListener` → `event/`
|
||||||
|
- [ ] Переместить `listener/MessageListener` → `event/`
|
||||||
|
- [ ] Переместить `listener/SlashCommandListener` → `event/`
|
||||||
|
- [ ] Переместить `listener/HelpMenuHandler` → `event/`
|
||||||
|
- [ ] Переместить `listener/ModerationLogger` → `event/`
|
||||||
|
- [ ] Удалить пустую папку `listener/`
|
||||||
|
|
||||||
|
**11. Реорганизовать core/**
|
||||||
|
- [ ] Создать `core/registry/`
|
||||||
|
- [ ] Переместить `core/CommandRegistry` → `core/registry/`
|
||||||
|
- [ ] Переместить `core/SlashCommandRegistry` → `core/registry/`
|
||||||
|
- [ ] Переместить `core/PrefixCommandRegistrar` → `core/registry/`
|
||||||
|
- [ ] Переместить `core/SlashCommandRegistrar` → `core/registry/`
|
||||||
|
- [ ] Создать `core/exception/`
|
||||||
|
- [ ] Переместить `exception/CommandExceptionHandler` → `core/exception/`
|
||||||
|
- [ ] Переместить `exception/CommandException` → `command/`
|
||||||
|
- [ ] Удалить пустую папку `exception/`
|
||||||
|
|
||||||
|
**12. Реорганизовать command/**
|
||||||
|
- [ ] Переименовать `command/impl/entertainment/interaction/` → `command/entertainment/social/`
|
||||||
|
- [ ] Переместить `command/impl/utility/MetricsCommand` → `command/system/`
|
||||||
|
- [ ] Переместить `command/impl/setup/LogSetupCommand` → `event/` (или удалить setup/)
|
||||||
|
|
||||||
|
#### Низкий приоритет (опционально):
|
||||||
|
|
||||||
|
**13. Создать repository/ (единый слой данных)**
|
||||||
|
- [ ] Создать `repository/`
|
||||||
|
- [ ] Создать интерфейсы `UserRepository`, `GuildConfigRepository`
|
||||||
|
- [ ] Создать `repository/impl/`
|
||||||
|
- [ ] Рефакторить `data/JsonDataStore` → `repository/impl/`
|
||||||
|
- [ ] Рефакторить `database/ConfigRepository` → `repository/impl/`
|
||||||
|
|
||||||
|
**14. Обновить документацию**
|
||||||
|
- [ ] Обновить FEATURES.md (убрать TTS)
|
||||||
|
- [ ] Обновить README.md (новая структура)
|
||||||
|
- [ ] Добавить ARCHITECTURE.md (описание доменов)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Основные задачи
|
||||||
|
|
||||||
### 1. ⚡ Переделать систему опыта (XP System Rework)
|
### 1. ⚡ Переделать систему опыта (XP System Rework)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
package creator.loki.audio;
|
|
||||||
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.playback.NonAllocatingAudioFrameBuffer;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class AudioManager {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AudioManager.class);
|
|
||||||
private static final long CLEANUP_INTERVAL_MINUTES = 10;
|
|
||||||
private static final long INACTIVE_THRESHOLD_MINUTES = 30;
|
|
||||||
|
|
||||||
private final AudioPlayerManager playerManager;
|
|
||||||
private final Map<Long, GuildMusicManager> musicManagers;
|
|
||||||
private final Map<Long, Long> lastActivityTime; // guildId -> timestamp
|
|
||||||
private final Map<Long, TtsQueue> ttsQueues; // guildId -> TtsQueue
|
|
||||||
private final ScheduledExecutorService cleanupScheduler;
|
|
||||||
|
|
||||||
public AudioManager() {
|
|
||||||
this.musicManagers = new ConcurrentHashMap<>();
|
|
||||||
this.lastActivityTime = new ConcurrentHashMap<>();
|
|
||||||
this.ttsQueues = new ConcurrentHashMap<>();
|
|
||||||
this.playerManager = new DefaultAudioPlayerManager();
|
|
||||||
this.playerManager.getConfiguration().setFrameBufferFactory(NonAllocatingAudioFrameBuffer::new);
|
|
||||||
AudioSourceManagers.registerRemoteSources(playerManager);
|
|
||||||
AudioSourceManagers.registerLocalSource(playerManager);
|
|
||||||
|
|
||||||
// Запускаем периодическую очистку неактивных менеджеров
|
|
||||||
this.cleanupScheduler = Executors.newSingleThreadScheduledExecutor();
|
|
||||||
this.cleanupScheduler.scheduleAtFixedRate(
|
|
||||||
this::cleanupInactiveManagers,
|
|
||||||
CLEANUP_INTERVAL_MINUTES,
|
|
||||||
CLEANUP_INTERVAL_MINUTES,
|
|
||||||
TimeUnit.MINUTES
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info("AudioManager инициализирован с автоочисткой каждые {} минут", CLEANUP_INTERVAL_MINUTES);
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized GuildMusicManager getGuildAudioPlayer(Guild guild) {
|
|
||||||
long guildId = guild.getIdLong();
|
|
||||||
GuildMusicManager musicManager = musicManagers.get(guildId);
|
|
||||||
|
|
||||||
if (musicManager == null) {
|
|
||||||
musicManager = new GuildMusicManager(playerManager);
|
|
||||||
musicManagers.put(guildId, musicManager);
|
|
||||||
logger.debug("Создан новый GuildMusicManager для гильдии: {}", guild.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем время последней активности
|
|
||||||
lastActivityTime.put(guildId, System.currentTimeMillis());
|
|
||||||
|
|
||||||
guild.getAudioManager().setSendingHandler(musicManager.getSendHandler());
|
|
||||||
return musicManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Очищает неактивные GuildMusicManager
|
|
||||||
*/
|
|
||||||
private void cleanupInactiveManagers() {
|
|
||||||
long currentTime = System.currentTimeMillis();
|
|
||||||
long thresholdTime = currentTime - TimeUnit.MINUTES.toMillis(INACTIVE_THRESHOLD_MINUTES);
|
|
||||||
|
|
||||||
musicManagers.entrySet().removeIf(entry -> {
|
|
||||||
long guildId = entry.getKey();
|
|
||||||
Long lastActivity = lastActivityTime.get(guildId);
|
|
||||||
|
|
||||||
if (lastActivity == null || lastActivity < thresholdTime) {
|
|
||||||
GuildMusicManager manager = entry.getValue();
|
|
||||||
|
|
||||||
// Проверяем, что плеер не активен
|
|
||||||
if (manager.getPlayer().getPlayingTrack() == null) {
|
|
||||||
logger.info("Удаление неактивного GuildMusicManager для гильдии ID: {}", guildId);
|
|
||||||
manager.getPlayer().destroy();
|
|
||||||
lastActivityTime.remove(guildId);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Принудительно удаляет GuildMusicManager для гильдии
|
|
||||||
*/
|
|
||||||
public synchronized void removeGuildAudioPlayer(long guildId) {
|
|
||||||
GuildMusicManager manager = musicManagers.remove(guildId);
|
|
||||||
if (manager != null) {
|
|
||||||
manager.getPlayer().destroy();
|
|
||||||
lastActivityTime.remove(guildId);
|
|
||||||
logger.debug("GuildMusicManager удалён для гильдии ID: {}", guildId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить или создать TtsQueue для гильдии
|
|
||||||
*/
|
|
||||||
public TtsQueue getTtsQueue(Guild guild) {
|
|
||||||
long guildId = guild.getIdLong();
|
|
||||||
return ttsQueues.computeIfAbsent(guildId, id -> {
|
|
||||||
logger.info("Создана новая TtsQueue для гильдии: {}", guild.getName());
|
|
||||||
return new TtsQueue();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Добавить TTS запрос в очередь
|
|
||||||
*/
|
|
||||||
public void enqueueTts(Guild guild, String text, net.dv8tion.jda.api.entities.channel.middleman.MessageChannel channel) {
|
|
||||||
GuildMusicManager musicManager = getGuildAudioPlayer(guild);
|
|
||||||
TtsQueue queue = getTtsQueue(guild);
|
|
||||||
|
|
||||||
TtsRequest request = new TtsRequest(this, musicManager, guild, text, channel);
|
|
||||||
queue.enqueue(request);
|
|
||||||
|
|
||||||
logger.info("TTS запрос добавлен для гильдии: {} (очередь: {})",
|
|
||||||
guild.getName(), queue.getQueueSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
public AudioPlayerManager getPlayerManager() {
|
|
||||||
return playerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Завершает работу AudioManager
|
|
||||||
*/
|
|
||||||
public void shutdown() {
|
|
||||||
logger.info("Завершение работы AudioManager...");
|
|
||||||
|
|
||||||
// Останавливаем все TTS очереди
|
|
||||||
ttsQueues.values().forEach(TtsQueue::shutdown);
|
|
||||||
ttsQueues.clear();
|
|
||||||
|
|
||||||
// Останавливаем scheduler
|
|
||||||
cleanupScheduler.shutdown();
|
|
||||||
try {
|
|
||||||
if (!cleanupScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
|
||||||
cleanupScheduler.shutdownNow();
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
cleanupScheduler.shutdownNow();
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Уничтожаем все плееры
|
|
||||||
musicManagers.values().forEach(manager -> manager.getPlayer().destroy());
|
|
||||||
musicManagers.clear();
|
|
||||||
lastActivityTime.clear();
|
|
||||||
|
|
||||||
logger.info("AudioManager завершён");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
package creator.loki.audio;
|
|
||||||
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame;
|
|
||||||
import net.dv8tion.jda.api.audio.AudioSendHandler;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
public class AudioPlayerSendHandler implements AudioSendHandler {
|
|
||||||
private final AudioPlayer audioPlayer;
|
|
||||||
private final ByteBuffer buffer;
|
|
||||||
private final MutableAudioFrame frame;
|
|
||||||
|
|
||||||
public AudioPlayerSendHandler(AudioPlayer audioPlayer) {
|
|
||||||
this.audioPlayer = audioPlayer;
|
|
||||||
this.buffer = ByteBuffer.allocate(1024);
|
|
||||||
this.frame = new MutableAudioFrame();
|
|
||||||
this.frame.setBuffer(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean canProvide() {
|
|
||||||
return audioPlayer.provide(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByteBuffer provide20MsAudio() {
|
|
||||||
buffer.flip();
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOpus() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
package creator.loki.audio;
|
|
||||||
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class GuildMusicManager {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GuildMusicManager.class);
|
|
||||||
private final AudioPlayer player;
|
|
||||||
private final AudioPlayerSendHandler sendHandler;
|
|
||||||
private Guild guild;
|
|
||||||
private boolean autoDisconnectEnabled = false;
|
|
||||||
|
|
||||||
public GuildMusicManager(AudioPlayerManager manager) {
|
|
||||||
this.player = manager.createPlayer();
|
|
||||||
this.sendHandler = new AudioPlayerSendHandler(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGuild(Guild guild) {
|
|
||||||
this.guild = guild;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void enableAutoDisconnect() {
|
|
||||||
if (autoDisconnectEnabled) {
|
|
||||||
return; // Уже включено
|
|
||||||
}
|
|
||||||
|
|
||||||
autoDisconnectEnabled = true;
|
|
||||||
player.addListener(new AudioEventAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) {
|
|
||||||
if (endReason.mayStartNext && guild != null) {
|
|
||||||
logger.info("TTS трек завершён, отключаемся от голосового канала");
|
|
||||||
guild.getAudioManager().closeAudioConnection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public AudioPlayer getPlayer() {
|
|
||||||
return player;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AudioPlayerSendHandler getSendHandler() {
|
|
||||||
return sendHandler;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
package creator.loki.audio;
|
|
||||||
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
public class TtsAudioSource {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(TtsAudioSource.class);
|
|
||||||
private static final String TTS_API = "https://translate.google.com/translate_tts";
|
|
||||||
|
|
||||||
public static void playTts(AudioManager audioManager, GuildMusicManager musicManager,
|
|
||||||
String text, MessageChannel channel) {
|
|
||||||
try {
|
|
||||||
// Генерируем URL для TTS
|
|
||||||
String encodedText = URLEncoder.encode(text, StandardCharsets.UTF_8);
|
|
||||||
String ttsUrl = TTS_API + "?ie=UTF-8&client=tw-ob&tl=ru&q=" + encodedText;
|
|
||||||
|
|
||||||
logger.info("Загрузка TTS: {}", ttsUrl);
|
|
||||||
|
|
||||||
// Загружаем напрямую из URL
|
|
||||||
audioManager.getPlayerManager().loadItem(ttsUrl, new AudioLoadResultHandler() {
|
|
||||||
@Override
|
|
||||||
public void trackLoaded(AudioTrack track) {
|
|
||||||
logger.info("TTS трек загружен успешно");
|
|
||||||
musicManager.getPlayer().playTrack(track);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void playlistLoaded(AudioPlaylist playlist) {
|
|
||||||
logger.info("TTS плейлист загружен");
|
|
||||||
if (!playlist.getTracks().isEmpty()) {
|
|
||||||
trackLoaded(playlist.getTracks().get(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void noMatches() {
|
|
||||||
logger.error("TTS: Не найдено совпадений");
|
|
||||||
channel.sendMessage("❌ Не удалось загрузить TTS").queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadFailed(FriendlyException exception) {
|
|
||||||
logger.error("TTS: Ошибка загрузки", exception);
|
|
||||||
channel.sendMessage("❌ Ошибка TTS: " + exception.getMessage()).queue();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("TTS: Ошибка генерации", e);
|
|
||||||
channel.sendMessage("❌ Ошибка генерации TTS: " + e.getMessage()).queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
package creator.loki.audio;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.concurrent.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Очередь TTS запросов с автоматическим отключением через 30 секунд бездействия
|
|
||||||
*/
|
|
||||||
public class TtsQueue {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(TtsQueue.class);
|
|
||||||
private static final int DISCONNECT_DELAY_SECONDS = 30;
|
|
||||||
|
|
||||||
private final BlockingQueue<TtsRequest> queue = new LinkedBlockingQueue<>();
|
|
||||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
|
||||||
private final ExecutorService processor = Executors.newSingleThreadExecutor();
|
|
||||||
|
|
||||||
private ScheduledFuture<?> disconnectTask;
|
|
||||||
private volatile boolean isProcessing = false;
|
|
||||||
private volatile boolean isShutdown = false;
|
|
||||||
|
|
||||||
public TtsQueue() {
|
|
||||||
startProcessor();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Добавить TTS запрос в очередь
|
|
||||||
*/
|
|
||||||
public void enqueue(TtsRequest request) {
|
|
||||||
if (isShutdown) {
|
|
||||||
logger.warn("TtsQueue уже завершена, запрос отклонён");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
queue.offer(request);
|
|
||||||
cancelDisconnectTimer();
|
|
||||||
logger.info("TTS запрос добавлен в очередь. Размер очереди: {}", queue.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запустить обработчик очереди
|
|
||||||
*/
|
|
||||||
private void startProcessor() {
|
|
||||||
processor.submit(() -> {
|
|
||||||
while (!isShutdown) {
|
|
||||||
try {
|
|
||||||
TtsRequest request = queue.poll(1, TimeUnit.SECONDS);
|
|
||||||
if (request != null) {
|
|
||||||
processRequest(request);
|
|
||||||
} else if (!isProcessing && queue.isEmpty()) {
|
|
||||||
scheduleDisconnect(request != null ? request : getLastRequest());
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
break;
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Ошибка обработки TTS запроса", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработать один TTS запрос
|
|
||||||
*/
|
|
||||||
private void processRequest(TtsRequest request) {
|
|
||||||
isProcessing = true;
|
|
||||||
cancelDisconnectTimer();
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info("Обработка TTS: {}", request.getText());
|
|
||||||
request.process();
|
|
||||||
|
|
||||||
// Ждём завершения воспроизведения
|
|
||||||
waitForPlaybackEnd(request);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Ошибка воспроизведения TTS", e);
|
|
||||||
} finally {
|
|
||||||
isProcessing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ждать завершения воспроизведения
|
|
||||||
*/
|
|
||||||
private void waitForPlaybackEnd(TtsRequest request) {
|
|
||||||
try {
|
|
||||||
// Ждём максимум 30 секунд
|
|
||||||
for (int i = 0; i < 30; i++) {
|
|
||||||
if (!request.isPlaying()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Thread.sleep(1000);
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запланировать отключение через 30 секунд
|
|
||||||
*/
|
|
||||||
private void scheduleDisconnect(TtsRequest lastRequest) {
|
|
||||||
if (disconnectTask != null && !disconnectTask.isDone()) {
|
|
||||||
return; // Уже запланировано
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectTask = scheduler.schedule(() -> {
|
|
||||||
if (queue.isEmpty() && !isProcessing && lastRequest != null) {
|
|
||||||
logger.info("30 секунд бездействия, отключаемся от голосового канала");
|
|
||||||
lastRequest.disconnect();
|
|
||||||
}
|
|
||||||
}, DISCONNECT_DELAY_SECONDS, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
logger.info("Запланировано отключение через {} секунд", DISCONNECT_DELAY_SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Отменить таймер отключения
|
|
||||||
*/
|
|
||||||
private void cancelDisconnectTimer() {
|
|
||||||
if (disconnectTask != null && !disconnectTask.isDone()) {
|
|
||||||
disconnectTask.cancel(false);
|
|
||||||
logger.debug("Таймер отключения отменён");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить последний запрос (для отключения)
|
|
||||||
*/
|
|
||||||
private TtsRequest getLastRequest() {
|
|
||||||
return queue.peek();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Завершить работу очереди
|
|
||||||
*/
|
|
||||||
public void shutdown() {
|
|
||||||
isShutdown = true;
|
|
||||||
cancelDisconnectTimer();
|
|
||||||
|
|
||||||
scheduler.shutdown();
|
|
||||||
processor.shutdown();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
|
||||||
scheduler.shutdownNow();
|
|
||||||
}
|
|
||||||
if (!processor.awaitTermination(5, TimeUnit.SECONDS)) {
|
|
||||||
processor.shutdownNow();
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
scheduler.shutdownNow();
|
|
||||||
processor.shutdownNow();
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("TtsQueue завершена");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить размер очереди
|
|
||||||
*/
|
|
||||||
public int getQueueSize() {
|
|
||||||
return queue.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверить, обрабатывается ли запрос
|
|
||||||
*/
|
|
||||||
public boolean isProcessing() {
|
|
||||||
return isProcessing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
package creator.loki.audio;
|
|
||||||
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Один TTS запрос
|
|
||||||
*/
|
|
||||||
public class TtsRequest {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(TtsRequest.class);
|
|
||||||
private static final String TTS_API = "https://translate.google.com/translate_tts";
|
|
||||||
|
|
||||||
private final AudioManager audioManager;
|
|
||||||
private final GuildMusicManager musicManager;
|
|
||||||
private final Guild guild;
|
|
||||||
private final String text;
|
|
||||||
private final MessageChannel channel;
|
|
||||||
|
|
||||||
private final CountDownLatch loadLatch = new CountDownLatch(1);
|
|
||||||
private volatile boolean isPlaying = false;
|
|
||||||
private volatile AudioTrack currentTrack;
|
|
||||||
|
|
||||||
public TtsRequest(AudioManager audioManager, GuildMusicManager musicManager,
|
|
||||||
Guild guild, String text, MessageChannel channel) {
|
|
||||||
this.audioManager = audioManager;
|
|
||||||
this.musicManager = musicManager;
|
|
||||||
this.guild = guild;
|
|
||||||
this.text = text;
|
|
||||||
this.channel = channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработать запрос (загрузить и воспроизвести)
|
|
||||||
*/
|
|
||||||
public void process() {
|
|
||||||
try {
|
|
||||||
String encodedText = URLEncoder.encode(text, StandardCharsets.UTF_8);
|
|
||||||
String ttsUrl = TTS_API + "?ie=UTF-8&client=tw-ob&tl=ru&q=" + encodedText;
|
|
||||||
|
|
||||||
logger.info("Загрузка TTS: {}", ttsUrl);
|
|
||||||
|
|
||||||
audioManager.getPlayerManager().loadItem(ttsUrl, new AudioLoadResultHandler() {
|
|
||||||
@Override
|
|
||||||
public void trackLoaded(AudioTrack track) {
|
|
||||||
logger.info("TTS трек загружен: {}", text);
|
|
||||||
currentTrack = track;
|
|
||||||
isPlaying = true;
|
|
||||||
|
|
||||||
AudioPlayer player = musicManager.getPlayer();
|
|
||||||
player.playTrack(track);
|
|
||||||
|
|
||||||
loadLatch.countDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void playlistLoaded(AudioPlaylist playlist) {
|
|
||||||
if (!playlist.getTracks().isEmpty()) {
|
|
||||||
trackLoaded(playlist.getTracks().get(0));
|
|
||||||
} else {
|
|
||||||
loadFailed(new FriendlyException("Плейлист пуст",
|
|
||||||
FriendlyException.Severity.COMMON, null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void noMatches() {
|
|
||||||
logger.error("TTS: Не найдено совпадений для '{}'", text);
|
|
||||||
channel.sendMessage("❌ Не удалось загрузить TTS").queue();
|
|
||||||
loadLatch.countDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadFailed(FriendlyException exception) {
|
|
||||||
logger.error("TTS: Ошибка загрузки '{}'", text, exception);
|
|
||||||
channel.sendMessage("❌ Ошибка TTS: " + exception.getMessage()).queue();
|
|
||||||
loadLatch.countDown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ждём загрузки (максимум 10 секунд)
|
|
||||||
if (!loadLatch.await(10, TimeUnit.SECONDS)) {
|
|
||||||
logger.warn("TTS: Таймаут загрузки для '{}'", text);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ждём завершения воспроизведения
|
|
||||||
waitForPlaybackEnd();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("TTS: Ошибка обработки '{}'", text, e);
|
|
||||||
channel.sendMessage("❌ Ошибка TTS: " + e.getMessage()).queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ждать завершения воспроизведения
|
|
||||||
*/
|
|
||||||
private void waitForPlaybackEnd() {
|
|
||||||
AudioPlayer player = musicManager.getPlayer();
|
|
||||||
|
|
||||||
// Ждём максимум 30 секунд
|
|
||||||
for (int i = 0; i < 30; i++) {
|
|
||||||
AudioTrack playingTrack = player.getPlayingTrack();
|
|
||||||
|
|
||||||
if (playingTrack == null || playingTrack != currentTrack) {
|
|
||||||
isPlaying = false;
|
|
||||||
logger.info("TTS воспроизведение завершено: {}", text);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Thread.sleep(1000);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isPlaying = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Отключиться от голосового канала
|
|
||||||
*/
|
|
||||||
public void disconnect() {
|
|
||||||
if (guild != null) {
|
|
||||||
guild.getAudioManager().closeAudioConnection();
|
|
||||||
logger.info("Отключились от голосового канала в гильдии: {}", guild.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверить, воспроизводится ли трек
|
|
||||||
*/
|
|
||||||
public boolean isPlaying() {
|
|
||||||
return isPlaying;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getText() {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Guild getGuild() {
|
|
||||||
return guild;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package creator.loki.command.impl.stats;
|
package creator.loki.command.impl.stats.leaderboard;
|
||||||
|
|
||||||
import creator.loki.level.LevelSystem;
|
import creator.loki.level.LevelSystem;
|
||||||
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package creator.loki.command.impl.stats;
|
package creator.loki.command.impl.stats.leaderboard;
|
||||||
|
|
||||||
import net.dv8tion.jda.api.interactions.components.buttons.Button;
|
import net.dv8tion.jda.api.interactions.components.buttons.Button;
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package creator.loki.command.impl.stats;
|
package creator.loki.command.impl.stats.leaderboard;
|
||||||
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
import creator.loki.embed.EmbedFactory;
|
||||||
import creator.loki.level.UserStats;
|
import creator.loki.level.UserStats;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package creator.loki.command.impl.stats;
|
package creator.loki.command.impl.stats.leaderboard;
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
package creator.loki.command.impl.utility;
|
|
||||||
|
|
||||||
import creator.loki.audio.AudioManager;
|
|
||||||
import creator.loki.command.Command;
|
|
||||||
import creator.loki.command.CommandContext;
|
|
||||||
import creator.loki.embed.EmbedTemplates;
|
|
||||||
import creator.loki.util.CooldownManager;
|
|
||||||
import net.dv8tion.jda.api.entities.GuildVoiceState;
|
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
|
||||||
|
|
||||||
public class TtsCommand implements Command {
|
|
||||||
private final CooldownManager cooldownManager;
|
|
||||||
private final AudioManager audioManager;
|
|
||||||
|
|
||||||
public TtsCommand(CooldownManager cooldownManager, AudioManager audioManager) {
|
|
||||||
this.cooldownManager = cooldownManager;
|
|
||||||
this.audioManager = audioManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "tts";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Бот заходит в голосовой канал и произносит текст";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUsage() {
|
|
||||||
return "tts <текст>";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getCategory() {
|
|
||||||
return "Развлечения";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(CommandContext context) {
|
|
||||||
if (context.getGuild() == null) {
|
|
||||||
context.getChannel().sendMessageEmbeds(
|
|
||||||
EmbedTemplates.error("Ошибка", "Команда доступна только на сервере")
|
|
||||||
).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.getArgs().isEmpty()) {
|
|
||||||
context.getChannel().sendMessageEmbeds(
|
|
||||||
EmbedTemplates.error("Ошибка", "Укажи текст для TTS")
|
|
||||||
).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Member member = context.getAuthor();
|
|
||||||
if (member == null) return;
|
|
||||||
|
|
||||||
// Проверка cooldown (15 секунд)
|
|
||||||
if (cooldownManager.isOnCooldown(member.getId(), "tts")) {
|
|
||||||
long remaining = cooldownManager.getRemainingSeconds(member.getId(), "tts");
|
|
||||||
context.getChannel().sendMessageEmbeds(
|
|
||||||
EmbedTemplates.error(
|
|
||||||
"Подожди!",
|
|
||||||
"Ты сможешь использовать TTS через " + remaining + " сек."
|
|
||||||
)
|
|
||||||
).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверка, что пользователь в голосовом канале
|
|
||||||
GuildVoiceState voiceState = member.getVoiceState();
|
|
||||||
if (voiceState == null || !voiceState.inAudioChannel()) {
|
|
||||||
context.getChannel().sendMessageEmbeds(
|
|
||||||
EmbedTemplates.error("Ошибка", "Ты должен быть в голосовом канале")
|
|
||||||
).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
VoiceChannel voiceChannel = voiceState.getChannel().asVoiceChannel();
|
|
||||||
String text = String.join(" ", context.getArgs());
|
|
||||||
|
|
||||||
if (text.length() > 200) {
|
|
||||||
context.getChannel().sendMessageEmbeds(
|
|
||||||
EmbedTemplates.error("Слишком длинно", "Максимум 200 символов")
|
|
||||||
).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Подключаемся к голосовому каналу (если ещё не подключены)
|
|
||||||
net.dv8tion.jda.api.managers.AudioManager audioManagerJda = context.getGuild().getAudioManager();
|
|
||||||
if (!audioManagerJda.isConnected()) {
|
|
||||||
audioManagerJda.openAudioConnection(voiceChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем TTS в очередь
|
|
||||||
audioManager.enqueueTts(context.getGuild(), text, context.getChannel());
|
|
||||||
|
|
||||||
cooldownManager.applyCooldown(member.getId(), "tts", 15);
|
|
||||||
|
|
||||||
context.getChannel().sendMessageEmbeds(
|
|
||||||
EmbedTemplates.success("TTS", "🔊 Добавлено в очередь: " + text)
|
|
||||||
).queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.entities.User;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Slash команда /avatar
|
|
||||||
*/
|
|
||||||
public class AvatarSlashCommand implements SlashCommand {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
User targetUser = event.getOption("user") != null
|
|
||||||
? event.getOption("user").getAsUser()
|
|
||||||
: event.getUser();
|
|
||||||
|
|
||||||
String avatarUrl = targetUser.getEffectiveAvatarUrl() + "?size=512";
|
|
||||||
|
|
||||||
EmbedBuilder embed = new EmbedBuilder()
|
|
||||||
.setTitle("🖼️ Аватар " + targetUser.getName())
|
|
||||||
.setImage(avatarUrl)
|
|
||||||
.setColor(new Color(88, 101, 242))
|
|
||||||
.setFooter("Запросил: " + event.getUser().getName());
|
|
||||||
|
|
||||||
event.replyEmbeds(embed.build()).queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("avatar", "Показывает аватар пользователя")
|
|
||||||
.addOption(OptionType.USER, "user", "Пользователь (по умолчанию - вы)", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "avatar";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Показывает аватар пользователя";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import creator.loki.embed.EmbedTemplates;
|
|
||||||
import net.dv8tion.jda.api.Permission;
|
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
public class BanSlashCommand implements SlashCommand {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
if (event.getGuild() == null) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Команда доступна только на сервере")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Member author = event.getMember();
|
|
||||||
if (author == null || !author.hasPermission(Permission.BAN_MEMBERS)) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "У вас нет прав на бан пользователей")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!event.getGuild().getSelfMember().hasPermission(Permission.BAN_MEMBERS)) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "У бота нет прав на бан пользователей")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Member target = event.getOption("user").getAsMember();
|
|
||||||
if (target == null) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Пользователь не найден")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.getId().equals(author.getId())) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Вы не можете забанить самого себя")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.getUser().isBot() && target.getId().equals(event.getGuild().getSelfMember().getId())) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Я не могу забанить самого себя")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!author.canInteract(target)) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "Вы не можете забанить этого пользователя")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!event.getGuild().getSelfMember().canInteract(target)) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "Я не могу забанить этого пользователя")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String reason = event.getOption("reason") != null
|
|
||||||
? event.getOption("reason").getAsString()
|
|
||||||
: "Причина не указана";
|
|
||||||
|
|
||||||
int deleteDays = event.getOption("delete_days") != null
|
|
||||||
? event.getOption("delete_days").getAsInt()
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
event.deferReply().queue();
|
|
||||||
|
|
||||||
event.getGuild().ban(target, deleteDays, java.util.concurrent.TimeUnit.DAYS)
|
|
||||||
.reason(reason)
|
|
||||||
.queue(
|
|
||||||
success -> event.getHook().editOriginalEmbeds(
|
|
||||||
EmbedTemplates.success(
|
|
||||||
"Пользователь забанен",
|
|
||||||
"**Пользователь:** " + target.getUser().getAsTag() + "\n" +
|
|
||||||
"**Причина:** " + reason + "\n" +
|
|
||||||
"**Удалено сообщений за дней:** " + deleteDays + "\n" +
|
|
||||||
"**Модератор:** " + author.getAsMention()
|
|
||||||
)
|
|
||||||
).queue(),
|
|
||||||
error -> event.getHook().editOriginalEmbeds(
|
|
||||||
EmbedTemplates.error("Ошибка", "Не удалось забанить: " + error.getMessage())
|
|
||||||
).queue()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("ban", "Банит пользователя на сервере")
|
|
||||||
.addOption(OptionType.USER, "user", "Пользователь для бана", true)
|
|
||||||
.addOption(OptionType.STRING, "reason", "Причина бана", false)
|
|
||||||
.addOption(OptionType.INTEGER, "delete_days", "Удалить сообщения за N дней (0-7)", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "ban";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Банит пользователя на сервере";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import creator.loki.embed.EmbedTemplates;
|
|
||||||
import net.dv8tion.jda.api.Permission;
|
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
public class ClearSlashCommand implements SlashCommand {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
if (event.getGuild() == null) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Команда доступна только на сервере")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Member author = event.getMember();
|
|
||||||
if (author == null || !author.hasPermission(Permission.MESSAGE_MANAGE)) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "У вас нет прав на управление сообщениями")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!event.getGuild().getSelfMember().hasPermission(Permission.MESSAGE_MANAGE)) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "У бота нет прав на управление сообщениями")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int amount = event.getOption("amount").getAsInt();
|
|
||||||
|
|
||||||
if (amount < 1 || amount > 100) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Количество должно быть от 1 до 100")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.deferReply(true).queue();
|
|
||||||
|
|
||||||
TextChannel channel = event.getChannel().asTextChannel();
|
|
||||||
channel.getHistory().retrievePast(amount).queue(
|
|
||||||
messages -> {
|
|
||||||
channel.purgeMessages(messages);
|
|
||||||
event.getHook().editOriginalEmbeds(
|
|
||||||
EmbedTemplates.success(
|
|
||||||
"Сообщения удалены",
|
|
||||||
"Удалено сообщений: " + messages.size()
|
|
||||||
)
|
|
||||||
).queue();
|
|
||||||
},
|
|
||||||
error -> event.getHook().editOriginalEmbeds(
|
|
||||||
EmbedTemplates.error("Ошибка", "Не удалось удалить сообщения: " + error.getMessage())
|
|
||||||
).queue()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("clear", "Удаляет указанное количество сообщений")
|
|
||||||
.addOption(OptionType.INTEGER, "amount", "Количество сообщений (1-100)", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "clear";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Удаляет указанное количество сообщений";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class CoinSlashCommand implements SlashCommand {
|
|
||||||
private static final Random random = new Random();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
boolean isHeads = random.nextBoolean();
|
|
||||||
String result = isHeads ? "Орёл 🦅" : "Решка 🪙";
|
|
||||||
|
|
||||||
EmbedBuilder embed = new EmbedBuilder()
|
|
||||||
.setTitle("🪙 Подбрасывание монетки")
|
|
||||||
.setDescription("Результат: **" + result + "**")
|
|
||||||
.setColor(isHeads ? new Color(255, 215, 0) : new Color(192, 192, 192));
|
|
||||||
|
|
||||||
event.replyEmbeds(embed.build()).queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("coin", "Подбросить монетку");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "coin";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Подбросить монетку";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class DiceSlashCommand implements SlashCommand {
|
|
||||||
private static final Random random = new Random();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
int result = random.nextInt(6) + 1;
|
|
||||||
String emoji = getDiceEmoji(result);
|
|
||||||
|
|
||||||
EmbedBuilder embed = new EmbedBuilder()
|
|
||||||
.setTitle("🎲 Бросок кубика")
|
|
||||||
.setDescription("Выпало: **" + result + "** " + emoji)
|
|
||||||
.setColor(new Color(88, 101, 242));
|
|
||||||
|
|
||||||
event.replyEmbeds(embed.build()).queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getDiceEmoji(int number) {
|
|
||||||
return switch (number) {
|
|
||||||
case 1 -> "⚀";
|
|
||||||
case 2 -> "⚁";
|
|
||||||
case 3 -> "⚂";
|
|
||||||
case 4 -> "⚃";
|
|
||||||
case 5 -> "⚄";
|
|
||||||
case 6 -> "⚅";
|
|
||||||
default -> "🎲";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("dice", "Бросить кубик");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "dice";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Бросить кубик";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class EightBallSlashCommand implements SlashCommand {
|
|
||||||
private static final Random random = new Random();
|
|
||||||
private static final String[] ANSWERS = {
|
|
||||||
"Бесспорно ✅",
|
|
||||||
"Предрешено ✅",
|
|
||||||
"Никаких сомнений ✅",
|
|
||||||
"Определённо да ✅",
|
|
||||||
"Можешь быть уверен в этом ✅",
|
|
||||||
"Мне кажется — да ✅",
|
|
||||||
"Вероятнее всего ✅",
|
|
||||||
"Хорошие перспективы ✅",
|
|
||||||
"Знаки говорят — да ✅",
|
|
||||||
"Да ✅",
|
|
||||||
"Пока не ясно 🤔",
|
|
||||||
"Спроси позже 🤔",
|
|
||||||
"Лучше не рассказывать 🤔",
|
|
||||||
"Сейчас нельзя предсказать 🤔",
|
|
||||||
"Сконцентрируйся и спроси опять 🤔",
|
|
||||||
"Даже не думай ❌",
|
|
||||||
"Мой ответ — нет ❌",
|
|
||||||
"По моим данным — нет ❌",
|
|
||||||
"Перспективы не очень хорошие ❌",
|
|
||||||
"Весьма сомнительно ❌"
|
|
||||||
};
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
var questionOption = event.getOption("question");
|
|
||||||
if (questionOption == null) {
|
|
||||||
event.reply("Задайте вопрос!").setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String question = questionOption.getAsString();
|
|
||||||
String answer = ANSWERS[random.nextInt(ANSWERS.length)];
|
|
||||||
|
|
||||||
EmbedBuilder embed = new EmbedBuilder()
|
|
||||||
.setTitle("🎱 Магический шар")
|
|
||||||
.addField("Вопрос", question, false)
|
|
||||||
.addField("Ответ", answer, false)
|
|
||||||
.setColor(new Color(88, 101, 242));
|
|
||||||
|
|
||||||
event.replyEmbeds(embed.build()).queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("8ball", "Задать вопрос магическому шару")
|
|
||||||
.addOption(OptionType.STRING, "question", "Ваш вопрос", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "8ball";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Задать вопрос магическому шару";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
|
|
||||||
public class HugSlashCommand implements SlashCommand {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
var targetOption = event.getOption("user");
|
|
||||||
if (targetOption == null) {
|
|
||||||
event.reply("Укажите пользователя!").setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var target = targetOption.getAsUser();
|
|
||||||
var author = event.getUser();
|
|
||||||
|
|
||||||
EmbedBuilder embed = new EmbedBuilder()
|
|
||||||
.setDescription(author.getAsMention() + " обнимает " + target.getAsMention() + " 🤗")
|
|
||||||
.setColor(new Color(255, 182, 193))
|
|
||||||
.setImage("https://media.tenor.com/images/hug.gif");
|
|
||||||
|
|
||||||
event.replyEmbeds(embed.build()).queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("hug", "Обнять пользователя")
|
|
||||||
.addOption(OptionType.USER, "user", "Кого обнять", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "hug";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Обнять пользователя";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import creator.loki.embed.EmbedTemplates;
|
|
||||||
import net.dv8tion.jda.api.Permission;
|
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
public class KickSlashCommand implements SlashCommand {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
if (event.getGuild() == null) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Команда доступна только на сервере")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Member author = event.getMember();
|
|
||||||
if (author == null || !author.hasPermission(Permission.KICK_MEMBERS)) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "У вас нет прав на кик пользователей")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!event.getGuild().getSelfMember().hasPermission(Permission.KICK_MEMBERS)) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "У бота нет прав на кик пользователей")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Member target = event.getOption("user").getAsMember();
|
|
||||||
if (target == null) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Пользователь не найден")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.getId().equals(author.getId())) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Вы не можете кикнуть самого себя")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.getUser().isBot() && target.getId().equals(event.getGuild().getSelfMember().getId())) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Я не могу кикнуть самого себя")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!author.canInteract(target)) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "Вы не можете кикнуть этого пользователя")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!event.getGuild().getSelfMember().canInteract(target)) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "Я не могу кикнуть этого пользователя")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String reason = event.getOption("reason") != null
|
|
||||||
? event.getOption("reason").getAsString()
|
|
||||||
: "Причина не указана";
|
|
||||||
|
|
||||||
event.deferReply().queue();
|
|
||||||
|
|
||||||
event.getGuild().kick(target)
|
|
||||||
.reason(reason)
|
|
||||||
.queue(
|
|
||||||
success -> event.getHook().editOriginalEmbeds(
|
|
||||||
EmbedTemplates.success(
|
|
||||||
"Пользователь кикнут",
|
|
||||||
"**Пользователь:** " + target.getUser().getAsTag() + "\n" +
|
|
||||||
"**Причина:** " + reason + "\n" +
|
|
||||||
"**Модератор:** " + author.getAsMention()
|
|
||||||
)
|
|
||||||
).queue(),
|
|
||||||
error -> event.getHook().editOriginalEmbeds(
|
|
||||||
EmbedTemplates.error("Ошибка", "Не удалось кикнуть: " + error.getMessage())
|
|
||||||
).queue()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("kick", "Кикает пользователя с сервера")
|
|
||||||
.addOption(OptionType.USER, "user", "Пользователь для кика", true)
|
|
||||||
.addOption(OptionType.STRING, "reason", "Причина кика", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "kick";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Кикает пользователя с сервера";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import creator.loki.metrics.MetricsCollector;
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.Permission;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Slash команда /metrics
|
|
||||||
*/
|
|
||||||
public class MetricsSlashCommand implements SlashCommand {
|
|
||||||
private final MetricsCollector metrics;
|
|
||||||
|
|
||||||
public MetricsSlashCommand(MetricsCollector metrics) {
|
|
||||||
this.metrics = metrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
// Только администраторы
|
|
||||||
if (event.getMember() == null || !event.getMember().hasPermission(Permission.ADMINISTRATOR)) {
|
|
||||||
event.reply("❌ Эта команда доступна только администраторам.")
|
|
||||||
.setEphemeral(true)
|
|
||||||
.queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String stats = metrics.getFullStats();
|
|
||||||
|
|
||||||
EmbedBuilder embed = new EmbedBuilder()
|
|
||||||
.setTitle("📊 Метрики бота")
|
|
||||||
.setDescription("```\n" + stats + "\n```")
|
|
||||||
.setColor(new Color(88, 101, 242))
|
|
||||||
.setFooter("Slash команда");
|
|
||||||
|
|
||||||
event.replyEmbeds(embed.build()).setEphemeral(true).queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("metrics", "Показывает метрики и статистику бота (только для админов)");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "metrics";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Показывает метрики и статистику бота";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.JDA;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Slash команда /ping
|
|
||||||
*/
|
|
||||||
public class PingSlashCommand implements SlashCommand {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
JDA jda = event.getJDA();
|
|
||||||
long gatewayPing = jda.getGatewayPing();
|
|
||||||
|
|
||||||
event.deferReply().queue(hook -> {
|
|
||||||
long restPing = hook.getInteraction().getJDA().getRestPing().complete();
|
|
||||||
|
|
||||||
EmbedBuilder embed = new EmbedBuilder()
|
|
||||||
.setTitle("🏓 Pong!")
|
|
||||||
.setColor(new Color(88, 101, 242))
|
|
||||||
.addField("Gateway Ping", gatewayPing + " ms", true)
|
|
||||||
.addField("REST Ping", restPing + " ms", true)
|
|
||||||
.setFooter("Slash команда");
|
|
||||||
|
|
||||||
hook.editOriginalEmbeds(embed.build()).queue();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("ping", "Проверяет задержку бота");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "ping";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Проверяет задержку бота";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import creator.loki.data.JsonDataStore;
|
|
||||||
import creator.loki.level.LevelSystem;
|
|
||||||
import creator.loki.profile.ProfileData;
|
|
||||||
import creator.loki.profile.ProfileService;
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.entities.User;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Slash команда /profile
|
|
||||||
*/
|
|
||||||
public class ProfileSlashCommand implements SlashCommand {
|
|
||||||
private final ProfileService profileService;
|
|
||||||
|
|
||||||
public ProfileSlashCommand(JsonDataStore dataStore) {
|
|
||||||
this.profileService = new ProfileService(dataStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
Member targetMember = event.getOption("user") != null
|
|
||||||
? event.getOption("user").getAsMember()
|
|
||||||
: event.getMember();
|
|
||||||
|
|
||||||
if (targetMember == null) {
|
|
||||||
event.reply("❌ Не удалось получить информацию о пользователе.")
|
|
||||||
.setEphemeral(true)
|
|
||||||
.queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProfileData profile = profileService.getProfileData(targetMember, event.getGuild());
|
|
||||||
|
|
||||||
// Создаём прогресс-бар
|
|
||||||
int progress = (int) ((double) profile.getCurrentXp() / profile.getNextLevelXp() * 20);
|
|
||||||
String progressBar = "▰".repeat(progress) + "▱".repeat(20 - progress);
|
|
||||||
|
|
||||||
EmbedBuilder embed = new EmbedBuilder()
|
|
||||||
.setTitle("📊 Профиль " + profile.getUsername())
|
|
||||||
.setThumbnail(profile.getAvatarUrl())
|
|
||||||
.setColor(new Color(138, 80, 255))
|
|
||||||
.addField("🎯 Уровень", String.valueOf(profile.getLevel()), true)
|
|
||||||
.addField("⭐ Ранг", "#" + profile.getRank(), true)
|
|
||||||
.addField("💬 Сообщений", String.valueOf(profile.getMessagesCount()), true)
|
|
||||||
.addField("🎤 Голос (мин)", String.valueOf(profile.getVoiceMinutes()), true)
|
|
||||||
.addField("👍 Репутация", String.valueOf(profile.getReputation()), true)
|
|
||||||
.addField("📈 Прогресс",
|
|
||||||
progressBar + "\n" + profile.getCurrentXp() + " / " + profile.getNextLevelXp() + " XP",
|
|
||||||
false)
|
|
||||||
.setFooter("Запросил: " + event.getUser().getName());
|
|
||||||
|
|
||||||
event.replyEmbeds(embed.build()).queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("profile", "Показывает профиль пользователя")
|
|
||||||
.addOption(OptionType.USER, "user", "Пользователь (по умолчанию - вы)", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "profile";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Показывает профиль пользователя";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import creator.loki.data.JsonDataStore;
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class RankSlashCommand implements SlashCommand {
|
|
||||||
private final JsonDataStore dataStore;
|
|
||||||
|
|
||||||
public RankSlashCommand(JsonDataStore dataStore) {
|
|
||||||
this.dataStore = dataStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
String userId = event.getUser().getId();
|
|
||||||
String levelKey = "level_" + userId;
|
|
||||||
String xpKey = "xp_" + userId;
|
|
||||||
|
|
||||||
Integer level = dataStore.getData(levelKey, Integer.class);
|
|
||||||
Integer xp = dataStore.getData(xpKey, Integer.class);
|
|
||||||
if (level == null) level = 1;
|
|
||||||
if (xp == null) xp = 0;
|
|
||||||
|
|
||||||
int xpNeeded = calculateXpForNextLevel(level);
|
|
||||||
|
|
||||||
EmbedBuilder embed = new EmbedBuilder()
|
|
||||||
.setTitle("📊 Ваш ранг")
|
|
||||||
.setDescription(event.getUser().getAsMention())
|
|
||||||
.addField("Уровень", String.valueOf(level), true)
|
|
||||||
.addField("Опыт", xp + " / " + xpNeeded, true)
|
|
||||||
.setColor(new Color(88, 101, 242))
|
|
||||||
.setThumbnail(event.getUser().getAvatarUrl());
|
|
||||||
|
|
||||||
event.replyEmbeds(embed.build()).queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int calculateXpForNextLevel(int level) {
|
|
||||||
return 100 * level;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("rank", "Показать ваш текущий ранг и уровень");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "rank";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Показать ваш текущий ранг и уровень";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import creator.loki.data.JsonDataStore;
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
|
|
||||||
public class RepSlashCommand implements SlashCommand {
|
|
||||||
private final JsonDataStore dataStore;
|
|
||||||
|
|
||||||
public RepSlashCommand(JsonDataStore dataStore) {
|
|
||||||
this.dataStore = dataStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
var targetOption = event.getOption("user");
|
|
||||||
if (targetOption == null) {
|
|
||||||
event.reply("Укажите пользователя!").setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var target = targetOption.getAsUser();
|
|
||||||
var author = event.getUser();
|
|
||||||
|
|
||||||
if (target.getId().equals(author.getId())) {
|
|
||||||
event.reply("Вы не можете дать репутацию самому себе!").setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.isBot()) {
|
|
||||||
event.reply("Нельзя давать репутацию ботам!").setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String key = "rep_" + target.getId();
|
|
||||||
Integer currentRep = dataStore.getData(key, Integer.class);
|
|
||||||
if (currentRep == null) currentRep = 0;
|
|
||||||
dataStore.setData(key, currentRep + 1);
|
|
||||||
|
|
||||||
EmbedBuilder embed = new EmbedBuilder()
|
|
||||||
.setTitle("✨ Репутация")
|
|
||||||
.setDescription(author.getAsMention() + " дал репутацию " + target.getAsMention())
|
|
||||||
.addField("Новая репутация", String.valueOf(currentRep + 1), false)
|
|
||||||
.setColor(new Color(0, 255, 0));
|
|
||||||
|
|
||||||
event.replyEmbeds(embed.build()).queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("rep", "Дать репутацию пользователю")
|
|
||||||
.addOption(OptionType.USER, "user", "Кому дать репутацию", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "rep";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Дать репутацию пользователю";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class ShipSlashCommand implements SlashCommand {
|
|
||||||
private static final Random random = new Random();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
var user1Option = event.getOption("user1");
|
|
||||||
var user2Option = event.getOption("user2");
|
|
||||||
|
|
||||||
if (user1Option == null || user2Option == null) {
|
|
||||||
event.reply("Укажите двух пользователей!").setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var user1 = user1Option.getAsUser();
|
|
||||||
var user2 = user2Option.getAsUser();
|
|
||||||
|
|
||||||
int percentage = random.nextInt(101);
|
|
||||||
String hearts = getHearts(percentage);
|
|
||||||
String comment = getComment(percentage);
|
|
||||||
|
|
||||||
EmbedBuilder embed = new EmbedBuilder()
|
|
||||||
.setTitle("💕 Ship-метр")
|
|
||||||
.setDescription(
|
|
||||||
user1.getAsMention() + " ❤️ " + user2.getAsMention() + "\n\n" +
|
|
||||||
hearts + " **" + percentage + "%**\n\n" +
|
|
||||||
comment
|
|
||||||
)
|
|
||||||
.setColor(getColor(percentage));
|
|
||||||
|
|
||||||
event.replyEmbeds(embed.build()).queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getHearts(int percentage) {
|
|
||||||
int hearts = percentage / 10;
|
|
||||||
return "❤️".repeat(hearts) + "🖤".repeat(10 - hearts);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getComment(int percentage) {
|
|
||||||
if (percentage >= 90) return "Идеальная пара! 💖";
|
|
||||||
if (percentage >= 70) return "Отличная совместимость! 💕";
|
|
||||||
if (percentage >= 50) return "Неплохие шансы! 💗";
|
|
||||||
if (percentage >= 30) return "Есть потенциал... 💔";
|
|
||||||
return "Лучше остаться друзьями 💔";
|
|
||||||
}
|
|
||||||
|
|
||||||
private Color getColor(int percentage) {
|
|
||||||
if (percentage >= 70) return new Color(255, 105, 180);
|
|
||||||
if (percentage >= 40) return new Color(255, 182, 193);
|
|
||||||
return new Color(128, 128, 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("ship", "Проверить совместимость двух пользователей")
|
|
||||||
.addOption(OptionType.USER, "user1", "Первый пользователь", true)
|
|
||||||
.addOption(OptionType.USER, "user2", "Второй пользователь", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "ship";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Проверить совместимость двух пользователей";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
package creator.loki.command.slash;
|
|
||||||
|
|
||||||
import creator.loki.command.SlashCommand;
|
|
||||||
import creator.loki.audio.AudioManager;
|
|
||||||
import creator.loki.embed.EmbedTemplates;
|
|
||||||
import net.dv8tion.jda.api.entities.GuildVoiceState;
|
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
|
||||||
|
|
||||||
public class TtsSlashCommand implements SlashCommand {
|
|
||||||
private final AudioManager audioManager;
|
|
||||||
|
|
||||||
public TtsSlashCommand(AudioManager audioManager) {
|
|
||||||
this.audioManager = audioManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) {
|
|
||||||
if (event.getGuild() == null) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Команда доступна только на сервере")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Member member = event.getMember();
|
|
||||||
if (member == null) {
|
|
||||||
event.reply("Ошибка получения информации о пользователе").setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GuildVoiceState voiceState = member.getVoiceState();
|
|
||||||
if (voiceState == null || !voiceState.inAudioChannel()) {
|
|
||||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Вы должны находиться в голосовом канале")).setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String text = event.getOption("text").getAsString();
|
|
||||||
if (text.length() > 200) {
|
|
||||||
event.reply("Текст слишком длинный! Максимум 200 символов.").setEphemeral(true).queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.reply("🔊 Воспроизведение TTS...").queue();
|
|
||||||
audioManager.enqueueTts(event.getGuild(), text, event.getChannel());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandData getCommandData() {
|
|
||||||
return Commands.slash("tts", "Озвучить текст в голосовом канале")
|
|
||||||
.addOption(OptionType.STRING, "text", "Текст для озвучки (макс. 200 символов)", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "tts";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Озвучить текст в голосовом канале";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,7 @@ import creator.loki.audio.AudioManager;
|
||||||
import creator.loki.command.impl.setup.CounterSetupCommand;
|
import creator.loki.command.impl.setup.CounterSetupCommand;
|
||||||
import creator.loki.command.impl.setup.LogSetupCommand;
|
import creator.loki.command.impl.setup.LogSetupCommand;
|
||||||
import creator.loki.command.impl.setup.VoiceSetupCommand;
|
import creator.loki.command.impl.setup.VoiceSetupCommand;
|
||||||
import creator.loki.command.impl.stats.LeaderboardButtonListener;
|
import creator.loki.command.impl.stats.leaderboard.LeaderboardButtonListener;
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
import creator.loki.counter.CounterManager;
|
import creator.loki.counter.CounterManager;
|
||||||
import creator.loki.data.JsonDataStore;
|
import creator.loki.data.JsonDataStore;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ 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 creator.loki.audio.AudioManager;
|
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
import creator.loki.config.ConfigLoader;
|
import creator.loki.config.ConfigLoader;
|
||||||
import creator.loki.core.CommandRegistry;
|
import creator.loki.core.CommandRegistry;
|
||||||
|
|
@ -38,7 +37,6 @@ public class BotModule extends AbstractModule {
|
||||||
bind(CooldownManager.class).in(Singleton.class);
|
bind(CooldownManager.class).in(Singleton.class);
|
||||||
bind(GlobalRateLimiter.class).in(Singleton.class);
|
bind(GlobalRateLimiter.class).in(Singleton.class);
|
||||||
bind(MetricsCollector.class).in(Singleton.class);
|
bind(MetricsCollector.class).in(Singleton.class);
|
||||||
bind(AudioManager.class).in(Singleton.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue