commit 0f664e317ed27505066a8f219c8d6ed19e3a469b Author: loki5512344 Date: Mon May 11 13:55:12 2026 +0200 Initial commit: Discord bot with XP system, voice channels, moderation logs, and member counter diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebf537d --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Gradle +.gradle/ +build/ +out/ +bin/ + +# IDE +.idea/ +*.iml +*.ipr +*.iws +.vscode/ + +# Config files with secrets +config/application.yml +.env +*.env + +# Data files +data/ + +# Logs +logs/ +*.log + +# OS +.DS_Store +Thumbs.db + +# Java +*.class +*.jar +!gradle-wrapper.jar diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 0000000..1cbead3 --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,235 @@ +# 🤖 Loki Discord Bot - Функции + +## 📋 Описание + +Многофункциональный Discord бот с системой уровней, личными голосовыми каналами, модерацией и развлекательными командами. + +**Технологии:** +- Java 21 +- JDA 5.0.0 +- SQLite + автомиграция +- Google Guice (DI) + +--- + +## ⭐ Основные функции + +### 🎯 Система уровней и опыта +- **Формула:** 200 XP × уровень +- **Получение XP:** + - 10 XP за сообщение (cooldown 60 сек) + - 1 XP за минуту в войсе +- **Команды:** + - `/rank [@user]` - карточка ранга + - `/leaderboard` - топ по уровням/сообщениям/войсу +- **Уведомления:** Красивые embed при повышении уровня +- **Награды:** Роли за достижение уровней (5, 10, 20, 30, 50) + +### 🎤 Личные голосовые каналы +- **Автосоздание:** Зайди в канал-триггер → создастся личный канал +- **Управление через кнопки:** + - 🔒 Закрыть/Открыть + - 👥 Лимит пользователей + - ✏️ Переименовать + - 🎨 Битрейт + - 🚫 Удалить +- **Автоудаление:** Пустые каналы удаляются автоматически +- **Настройка:** `/voice-setup` + +### 📊 Счетчик участников +- **Автообновление:** Название канала с количеством участников +- **Формат:** `【👥】Участники: 123` +- **Статистика:** Присоединения/выходы за день +- **История:** 30 дней +- **Настройка:** `/counter-setup` + +### 📝 Логи модерации +- **Что логируется:** + - 🗑️ Удаление сообщений + - 🔨 Баны/разбаны + - 👢 Кики + - 🔇 Муты/размуты + - 🎤 Отключение микрофона/звука +- **Настройка:** `/log-setup moderation` + +### 🎮 Развлекательные команды + +**Игры:** +- `/coin` - подбросить монетку +- `/dice` - бросить кубик +- `/8ball` - магический шар +- `/ship @user1 @user2` - совместимость +- `/rate` - оценить что-то +- `/gay/@fat/@iq/@pp` - процентомеры + +**Взаимодействия:** +- `/hug/@kiss/@pat/@poke` - обнять/поцеловать/погладить/ткнуть +- `/slap/@tickle/@cuddle` - шлепнуть/пощекотать/обнять +- `/compliment/@roast` - комплимент/жарка +- `/feed` - покормить + +### 🛡️ Модерация +- `/ban/@kick` - бан/кик пользователя +- `/clear ` - очистить сообщения +- Автологирование всех действий + +### 🎨 Кастомные роли +- `/createrole` - создать роль +- `/deleterole` - удалить роль +- `/editrole` - редактировать роль +- `/myroles` - мои роли + +### 📈 Статистика +- `/stats [@user]` - статистика пользователя +- `/rep/@minusrep` - репутация +- `/reptop` - топ по репутации +- `/serverstats` - статистика сервера + +### 🔧 Утилиты +- `/avatar [@user]` - аватар пользователя +- `/info [@user]` - информация о пользователе +- `/poll` - создать голосование +- `/ping` - пинг бота +- `/help` - справка +- `/tts ` - озвучить текст + +--- + +## ⚙️ Команды настройки (только админы) + +### Личные войс-каналы +``` +/voice-setup trigger - канал-триггер +/voice-setup category - категория для каналов +/voice-setup text-channel - текстовый канал для панелей +/voice-setup info - показать настройки +``` + +### Счетчик участников +``` +/counter-setup channel - установить канал +/counter-setup format - формат ({count}) +/counter-setup info - показать настройки +``` + +### Логирование +``` +/log-setup moderation - канал логов модерации +/log-setup levelup - канал уведомлений level up +/log-setup info - показать настройки +``` + +--- + +## 🗄️ База данных + +**SQLite** с автоматической миграцией из JSON: +- `config` - конфигурация +- `user_stats` - статистика пользователей +- `reputation` - репутация +- `ship_pairs` - ship пары +- `custom_roles` - кастомные роли +- `voice_channels` - личные каналы +- `counter_stats` - статистика счетчика + +--- + +## 🚀 Быстрый старт + +1. **Запуск:** +```bash +gradle run +``` + +2. **Настройка:** +``` +/voice-setup trigger #создать-канал +/voice-setup category Личные каналы +/voice-setup text-channel #управление + +/counter-setup channel #счетчик +/counter-setup format 【👥】Участники: {count} + +/log-setup moderation #логи +/log-setup levelup #level-up +``` + +3. **Готово!** Зайди в канал-триггер → создастся личный канал + +--- + +## 📦 Структура проекта + +``` +src/main/java/creator/loki/ +├── audio/ # TTS и аудио +├── command/ # Команды +│ └── impl/ +│ ├── entertainment/ # Игры и взаимодействия +│ ├── moderation/ # Модерация +│ ├── roles/ # Кастомные роли +│ ├── setup/ # Настройка +│ ├── stats/ # Статистика +│ └── utility/ # Утилиты +├── config/ # Конфигурация +├── core/ # Ядро бота +├── counter/ # Счетчик участников +├── data/ # JSON хранилище +├── database/ # SQLite + миграция +├── level/ # Система уровней +├── listener/ # Event listeners +├── voice/ # Личные каналы +└── util/ # Утилиты +``` + +--- + +## 🎯 Особенности + +✅ **Dependency Injection** (Google Guice) +✅ **SQLite** с автомиграцией из JSON +✅ **Graceful shutdown** всех компонентов +✅ **Логирование** (SLF4J + Logback) +✅ **Кэширование** (Caffeine) +✅ **Rate limiting** и cooldowns +✅ **Метрики** производительности +✅ **Модульная архитектура** + +--- + +## 📝 Права бота + +Необходимые права: +- ✅ `MANAGE_CHANNELS` - создание/удаление каналов +- ✅ `MOVE_MEMBERS` - перемещение пользователей +- ✅ `MANAGE_PERMISSIONS` - выдача прав +- ✅ `MANAGE_ROLES` - управление ролями +- ✅ `BAN_MEMBERS` - баны +- ✅ `KICK_MEMBERS` - кики +- ✅ `MANAGE_MESSAGES` - удаление сообщений +- ✅ `VIEW_AUDIT_LOG` - просмотр логов + +--- + +## 🔄 Обновления + +### v2.0 (2026-05-11) +- ✅ SQLite база данных +- ✅ Автомиграция из JSON +- ✅ Личные голосовые каналы +- ✅ Счетчик участников +- ✅ Логи модерации +- ✅ Уведомления level up +- ✅ Обработчик кнопок для войс-каналов + +### v1.0 +- Базовая система уровней +- Команды развлечений +- Модерация +- Кастомные роли + +--- + +**Версия:** 2.0 +**Дата:** 2026-05-11 +**Автор:** Loki Bot Team diff --git a/README.md b/README.md new file mode 100644 index 0000000..95667ff --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# 🤖 Loki Discord Bot + +Многофункциональный Discord бот с системой уровней, личными голосовыми каналами и модерацией. + +## ⚡ Быстрый старт + +```bash +# Запуск +gradle run + +# Настройка через Discord +/voice-setup trigger #создать-канал +/counter-setup channel #счетчик +/log-setup moderation #логи +``` + +## 📚 Документация + +- **[FEATURES.md](FEATURES.md)** - Полный список функций +- **[TODO.md](TODO.md)** - План развития + +## 🎯 Основные функции + +- 🎯 Система уровней (200 XP × уровень) +- 🎤 Личные голосовые каналы +- 📊 Счетчик участников +- 📝 Логи модерации +- 🎮 Развлекательные команды +- 🛡️ Модерация +- 🎨 Кастомные роли + +## 🛠️ Технологии + +- Java 21 +- JDA 5.0.0 +- SQLite +- Google Guice + +## 📦 Требования + +- Java 21+ +- Gradle 8.x + +--- + +**Версия:** 2.0 | **Лицензия:** MIT diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..d7709ac --- /dev/null +++ b/TODO.md @@ -0,0 +1,275 @@ +# TODO - План реализации функций + +## 🎯 Основные задачи + +### 1. ⚡ Переделать систему опыта (XP System Rework) + +**Текущая проблема:** +- Формула: `XP = 100 * level^1.5` (слишком сложная) +- XP за сообщение: 10 XP +- XP за минуту в войсе: 5 XP +- Cooldown на сообщения: 60 секунд + +**Новая система (из FEATURES.md):** +- ✅ **Формула прогрессии:** `XP для следующего уровня = 200 * текущий_уровень` + - Пример: уровень 1 → 2 требует 200 XP + - Уровень 5 → 6 требует 1000 XP + - Уровень 10 → 11 требует 2000 XP + +- ✅ **Голосовая активность:** + - 1 XP каждую минуту нахождения в голосовом канале + - Автоматическое отслеживание через `VoiceXpListener` + - Уведомление при повышении уровня в войс-канале + +**Файлы для изменения:** +- `src/main/java/creator/loki/level/LevelSystem.java` - изменить формулу +- `src/main/java/creator/loki/listener/VoiceTracker.java` - изменить начисление XP +- Добавить уведомления о level up в войсе + +**Статус:** ✅ **ГОТОВО** + +--- + +### 2. 🎤 Система личных голосовых каналов (Voice Management System) + +**Концепция:** +Пользователь заходит в специальный канал-триггер → бот автоматически создает личный войс-канал → перемещает пользователя туда → пишет сообщение в чат войса с настройками + +**Функционал:** + +#### 2.1 Автоматическое создание каналов +- ✅ Создать `VoiceChannelManager` класс +- ✅ Listener на вход в канал-триггер (например, "➕ Создать канал") +- ✅ Автоматическое создание канала с именем пользователя +- ✅ Перемещение пользователя в созданный канал +- ✅ Выдача прав управления владельцу канала +- ✅ Автоматическое удаление канала когда он пустой + +#### 2.2 Сообщение с настройками в чате войса +При создании канала бот отправляет embed с кнопками: +- 🔒 **Закрыть/Открыть** - переключение приватности +- 👥 **Лимит** - установить лимит пользователей (0-99) +- ✏️ **Переименовать** - изменить название канала +- 🎨 **Битрейт** - изменить качество звука +- 🚫 **Удалить** - удалить канал + +#### 2.3 Система пресетов +- ✅ Сохранение настроек как пресет +- ✅ Быстрое применение сохраненных настроек +- ✅ Хранение в `data/voice_presets.json` + +**Структура данных:** +```json +{ + "voice_channels": { + "channel_id": { + "owner_id": "user_id", + "created_at": 1234567890, + "settings": { + "name": "Канал пользователя", + "limit": 5, + "bitrate": 64000, + "locked": false + } + } + }, + "presets": { + "user_id": [ + { + "name": "Gaming", + "limit": 4, + "bitrate": 96000 + } + ] + } +} +``` + +**Файлы для создания:** +- `src/main/java/creator/loki/voice/VoiceChannelManager.java` +- `src/main/java/creator/loki/voice/VoiceChannelData.java` +- `src/main/java/creator/loki/voice/VoicePreset.java` +- `src/main/java/creator/loki/listener/VoiceChannelListener.java` +- `src/main/java/creator/loki/command/impl/voice/VoiceSettingsCommand.java` (для кнопок) + +**Статус:** ✅ **ГОТОВО** (базовая реализация, кнопки управления требуют обработчиков) + +--- + +### 3. 📊 Счетчик участников (Member Counter) + +**Функционал:** +- ✅ Обновление названия голосового канала с количеством участников +- ✅ Формат: `【👥】Участники: 123` +- ✅ Отслеживание дневной статистики: + - Количество присоединившихся за день + - Количество покинувших за день + - Разница за 24 часа +- ✅ Обновление каждые 5 минут +- ✅ Сохранение статистики в `data/counter_data.json` + +**Структура данных:** +```json +{ + "counter_channel_id": "1234567890", + "daily_stats": { + "date": "2026-05-11", + "joined": 15, + "left": 8, + "difference": 7 + }, + "history": [ + { + "date": "2026-05-10", + "joined": 20, + "left": 12, + "difference": 8 + } + ] +} +``` + +**Файлы для создания:** +- `src/main/java/creator/loki/listener/MemberCounterListener.java` +- `src/main/java/creator/loki/counter/CounterManager.java` +- `src/main/java/creator/loki/counter/DailyStats.java` + +**Статус:** 🔴 Не начато + +--- + +## ✅ Что уже сделано + +### ✅ Система опыта переделана +- Новая формула: `XP для уровня N = 200 * N` +- 1 XP за минуту в войсе +- Обновление каждую минуту вместо 5 минут +- Метод `addVoiceTime` теперь возвращает `LevelUpResult` для уведомлений + +### ✅ Система личных войс-каналов (базовая) +- `VoiceChannelManager` - управление каналами +- `VoiceChannelListener` - автоматическое создание/удаление +- `VoiceChannelData` и `VoicePreset` - модели данных +- Автоматическое создание при входе в триггер-канал +- Автоматическое удаление пустых каналов +- Панель управления с кнопками (требуется обработчик кнопок) + +### ✅ Счетчик участников +- `CounterManager` - управление счетчиком +- `MemberCounterListener` - отслеживание входов/выходов +- `DailyStats` - дневная статистика +- Обновление каждые 5 минут +- История за 30 дней + +### ✅ SQLite база данных + автомиграция +- `Database` - менеджер SQLite +- `DatabaseMigration` - автоматическая миграция из JSON +- `ConfigRepository` - работа с конфигурацией +- Все таблицы созданы автоматически +- Миграция данных при первом запуске + +### ✅ Команды настройки +- `/voice-setup trigger` - установить канал-триггер +- `/voice-setup category` - установить категорию +- `/voice-setup text-channel` - установить текстовый канал +- `/voice-setup info` - показать конфигурацию +- `/counter-setup channel` - установить канал счетчика +- `/counter-setup format` - установить формат +- `/counter-setup info` - показать конфигурацию + +--- + +## 🔧 Дополнительные улучшения + +### 4. 🎨 Улучшение уведомлений о level up +- ✅ Красивые embed сообщения при повышении уровня +- ✅ Отправка в тот же канал где получен XP +- ✅ Для войса - отправка в текстовый канал связанный с войсом +- ✅ Показывать прогресс до следующего уровня + +**Статус:** 🔴 Не начато + +--- + +### 5. 🛠️ Команды для настройки систем (Admin Commands) + +#### Voice System Setup: +- `/voice-setup trigger ` - установить канал-триггер +- `/voice-setup category ` - установить категорию для личных каналов +- `/voice-setup text-channel ` - установить текстовый канал для уведомлений + +#### Counter Setup: +- `/counter-setup channel ` - установить канал для счетчика +- `/counter-setup format ` - изменить формат отображения +- `/counter stats` - показать статистику за период + +**Статус:** 🔴 Не начато + +--- + +## 📝 Приоритеты реализации + +### Высокий приоритет (сделать в первую очередь): +1. ⚡ Переделать систему опыта (простая формула) +2. 🎤 Базовая система личных войс-каналов (создание/удаление) +3. 📊 Счетчик участников + +### Средний приоритет: +4. 🎤 Настройки войс-каналов через кнопки +5. 🎨 Улучшение уведомлений о level up +6. 🎤 Система пресетов для войс-каналов + +### Низкий приоритет: +7. 🛠️ Админ команды для настройки + +--- + +## 🐛 Известные проблемы (Bugs to Fix) + +### Критические: +- [ ] Проверить корректность начисления XP в войсе +- [ ] Убедиться что VoiceTracker правильно отслеживает время + +### Некритические: +- [ ] Удалить неиспользуемый logger в LevelSystem.java +- [ ] Убрать ненужный @SuppressWarnings в LevelSystem.java + +--- + +## 📚 Технические детали + +### Зависимости: +- JDA 5.0.0 (уже есть) +- Guice для DI (уже есть) +- Gson для JSON (уже есть) + +### Конфигурация (добавить в config): +```yaml +voice: + trigger_channel_id: 1234567890 + category_id: 1234567890 + text_channel_id: 1234567890 + auto_delete_empty: true + +counter: + channel_id: 1234567890 + update_interval: 300 # секунды + format: "【👥】Участники: {count}" +``` + +--- + +## ✅ Чеклист перед релизом + +- [ ] Все функции протестированы +- [ ] Логирование работает корректно +- [ ] Нет утечек памяти (проверить scheduler'ы) +- [ ] Graceful shutdown для всех scheduler'ов +- [ ] Документация обновлена +- [ ] FEATURES.md актуализирован + +--- + +**Дата создания:** 2026-05-11 +**Версия:** 1.0 +**Автор:** Loki Bot Team diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..57859aa --- /dev/null +++ b/build.gradle @@ -0,0 +1,75 @@ +plugins { + id 'java' + id 'application' +} + +group = 'creator.loki' +version = '1.0.0' + +repositories { + mavenCentral() +} + +dependencies { + // JDA - Discord API + implementation('net.dv8tion:JDA:5.0.0-beta.20') { + exclude module: 'opus-java' + } + + // LavaPlayer для голосового TTS + implementation 'dev.arbjerg:lavaplayer:2.2.2' + + // dotenv для загрузки .env + implementation 'io.github.cdimascio:dotenv-java:3.0.0' + + // Logging + implementation 'org.slf4j:slf4j-api:2.0.12' + implementation 'ch.qos.logback:logback-classic:1.5.3' + + // JSON + implementation 'com.google.code.gson:gson:2.10.1' + + // Caffeine Cache + implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' + + // Google Guice - Dependency Injection + implementation 'com.google.inject:guice:7.0.0' + + // SQLite Database + implementation 'org.xerial:sqlite-jdbc:3.45.1.0' + + // Testing + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testImplementation 'org.mockito:mockito-core:5.11.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +test { + useJUnitPlatform() +} + +application { + mainClass = 'creator.loki.App' +} + +jar { + manifest { + attributes( + 'Main-Class': 'creator.loki.App' + ) + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..4003b72 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,10 @@ +# Dependencies +jdaVersion=5.0.0-beta.20 +dotenvVersion=3.0.0 +slf4jVersion=2.0.12 +logbackVersion=1.5.3 + +# Gradle +org.gradle.jvmargs=-Xmx2048m +org.gradle.parallel=true +org.gradle.caching=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1af9e09 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..3b2e351 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'discord-bot' diff --git a/src/main/java/creator/loki/App.java b/src/main/java/creator/loki/App.java new file mode 100644 index 0000000..faff130 --- /dev/null +++ b/src/main/java/creator/loki/App.java @@ -0,0 +1,36 @@ +package creator.loki; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import creator.loki.core.BotInitializer; +import creator.loki.di.BotModule; +import net.dv8tion.jda.api.JDA; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class App { + private static final Logger logger = LoggerFactory.getLogger(App.class); + + public static void main(String[] args) { + try { + logger.info("Запуск Discord бота с Dependency Injection..."); + + // Создаём Guice Injector с модулем конфигурации + Injector injector = Guice.createInjector(new BotModule()); + + // Получаем BotInitializer с автоматически внедрёнными зависимостями + BotInitializer initializer = injector.getInstance(BotInitializer.class); + + // Запускаем бота + JDA jda = initializer.start(); + + // Ждём полной загрузки + jda.awaitReady(); + logger.info("Бот полностью готов к работе!"); + + } catch (Exception e) { + logger.error("Критическая ошибка при запуске бота", e); + System.exit(1); + } + } +} diff --git a/src/main/java/creator/loki/audio/AudioManager.java b/src/main/java/creator/loki/audio/AudioManager.java new file mode 100644 index 0000000..c85bdce --- /dev/null +++ b/src/main/java/creator/loki/audio/AudioManager.java @@ -0,0 +1,163 @@ +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 musicManagers; + private final Map lastActivityTime; // guildId -> timestamp + private final Map 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 завершён"); + } +} diff --git a/src/main/java/creator/loki/audio/AudioPlayerSendHandler.java b/src/main/java/creator/loki/audio/AudioPlayerSendHandler.java new file mode 100644 index 0000000..1a2db11 --- /dev/null +++ b/src/main/java/creator/loki/audio/AudioPlayerSendHandler.java @@ -0,0 +1,36 @@ +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; + } +} diff --git a/src/main/java/creator/loki/audio/GuildMusicManager.java b/src/main/java/creator/loki/audio/GuildMusicManager.java new file mode 100644 index 0000000..0762784 --- /dev/null +++ b/src/main/java/creator/loki/audio/GuildMusicManager.java @@ -0,0 +1,52 @@ +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; + } +} diff --git a/src/main/java/creator/loki/audio/TtsAudioSource.java b/src/main/java/creator/loki/audio/TtsAudioSource.java new file mode 100644 index 0000000..d2f11ea --- /dev/null +++ b/src/main/java/creator/loki/audio/TtsAudioSource.java @@ -0,0 +1,61 @@ +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(); + } + } +} diff --git a/src/main/java/creator/loki/audio/TtsQueue.java b/src/main/java/creator/loki/audio/TtsQueue.java new file mode 100644 index 0000000..dc7dd73 --- /dev/null +++ b/src/main/java/creator/loki/audio/TtsQueue.java @@ -0,0 +1,176 @@ +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 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; + } +} diff --git a/src/main/java/creator/loki/audio/TtsRequest.java b/src/main/java/creator/loki/audio/TtsRequest.java new file mode 100644 index 0000000..923f58d --- /dev/null +++ b/src/main/java/creator/loki/audio/TtsRequest.java @@ -0,0 +1,158 @@ +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; + } +} diff --git a/src/main/java/creator/loki/command/Command.java b/src/main/java/creator/loki/command/Command.java new file mode 100644 index 0000000..d515fca --- /dev/null +++ b/src/main/java/creator/loki/command/Command.java @@ -0,0 +1,16 @@ +package creator.loki.command; + +public interface Command { + String getName(); + String getDescription(); + + default String getUsage() { + return getName(); + } + + default String getCategory() { + return "Общие"; + } + + void execute(CommandContext context); +} diff --git a/src/main/java/creator/loki/command/CommandContext.java b/src/main/java/creator/loki/command/CommandContext.java new file mode 100644 index 0000000..003314a --- /dev/null +++ b/src/main/java/creator/loki/command/CommandContext.java @@ -0,0 +1,44 @@ +package creator.loki.command; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; + +import java.util.List; + +public class CommandContext { + private final Message message; + private final List args; + private final MessageChannel channel; + private final Member author; + private final Guild guild; + + public CommandContext(Message message, List args, MessageChannel channel, Member author, Guild guild) { + this.message = message; + this.args = args; + this.channel = channel; + this.author = author; + this.guild = guild; + } + + public Message getMessage() { + return message; + } + + public List getArgs() { + return args; + } + + public MessageChannel getChannel() { + return channel; + } + + public Member getAuthor() { + return author; + } + + public Guild getGuild() { + return guild; + } +} diff --git a/src/main/java/creator/loki/command/SlashCommand.java b/src/main/java/creator/loki/command/SlashCommand.java new file mode 100644 index 0000000..0410640 --- /dev/null +++ b/src/main/java/creator/loki/command/SlashCommand.java @@ -0,0 +1,29 @@ +package creator.loki.command; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; + +/** + * Интерфейс для slash команд + */ +public interface SlashCommand { + /** + * Выполняет slash команду + */ + void execute(SlashCommandInteractionEvent event); + + /** + * Возвращает данные команды для регистрации в Discord + */ + CommandData getCommandData(); + + /** + * Название команды + */ + String getName(); + + /** + * Описание команды + */ + String getDescription(); +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/games/BoykisserCommand.java b/src/main/java/creator/loki/command/impl/entertainment/games/BoykisserCommand.java new file mode 100644 index 0000000..ea5d037 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/games/BoykisserCommand.java @@ -0,0 +1,119 @@ +package creator.loki.command.impl.entertainment.games; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; +import java.util.Random; + +/** + * Boykisser метр с детерминированным результатом + */ +public class BoykisserCommand implements Command { + + private final CooldownManager cooldownManager; + + public BoykisserCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "boykisser"; + } + + @Override + public String getDescription() { + return "На сколько ты boykisser (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "boykisser [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + // Проверка cooldown (5 секунд) + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "boykisser")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "boykisser"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); + + if (target == null) return; + + if (target.getUser().isBot()) { + context.getChannel().sendMessage("❌ Боты не целуют мальчиков!").queue(); + return; + } + + // Применяем cooldown + cooldownManager.applyCooldown(context.getAuthor().getId(), "boykisser", 5); + + // Детерминированный процент + int boykisserLevel = getDeterministicValue(target.getId(), 777, 0, 100); + + // Прогресс-бар с поцелуями + StringBuilder bar = new StringBuilder(); + int filled = boykisserLevel / 10; + for (int i = 0; i < filled; i++) { + bar.append("💋"); + } + for (int i = filled; i < 10; i++) { + bar.append("⬜"); + } + + // Комментарий + String comment; + Color color; + if (boykisserLevel >= 90) { + comment = "😳 ЛЕГЕНДАРНЫЙ BOYKISSER!"; + color = new Color(255, 105, 180); + } else if (boykisserLevel >= 70) { + comment = "💕 Настоящий boykisser"; + color = new Color(255, 182, 193); + } else if (boykisserLevel >= 50) { + comment = "😊 Любит целовать мальчиков"; + color = new Color(255, 192, 203); + } else if (boykisserLevel >= 30) { + comment = "🙂 Иногда целует"; + color = new Color(176, 224, 230); + } else if (boykisserLevel >= 10) { + comment = "😐 Редко целует"; + color = new Color(135, 206, 235); + } else { + comment = "😶 Не boykisser"; + color = new Color(128, 128, 128); + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("💋 Boykisser Метр") + .setDescription(target.getAsMention()) + .addField("Уровень", bar.toString() + "\n**" + boykisserLevel + "%** - " + comment, false) + .setThumbnail(target.getUser().getAvatarUrl()) + .setColor(color) + .setFooter("💡 Детерминировано вашим Discord ID") + .build() + ).queue(); + } + + private int getDeterministicValue(String userId, int salt, int min, int max) { + long seed = Long.parseLong(userId) + salt; + Random random = new Random(seed); + return random.nextInt(max - min + 1) + min; + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/games/CoinCommand.java b/src/main/java/creator/loki/command/impl/entertainment/games/CoinCommand.java new file mode 100644 index 0000000..c57a1f3 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/games/CoinCommand.java @@ -0,0 +1,47 @@ +package creator.loki.command.impl.entertainment.games; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; + +import java.awt.Color; +import java.util.Random; + +public class CoinCommand implements Command { + private final Random random = new Random(); + + @Override + public String getName() { + return "coin"; + } + + @Override + public String getDescription() { + return "Подбросить монетку"; + } + + @Override + public String getUsage() { + return "coin"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + boolean isHeads = random.nextBoolean(); + String result = isHeads ? "Орёл" : "Решка"; + String emoji = isHeads ? "🦅" : "🪙"; + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle(emoji + " Подбрасываю монетку...") + .setDescription("**Результат:** " + result) + .setColor(isHeads ? new Color(255, 215, 0) : new Color(192, 192, 192)) + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/games/DiceCommand.java b/src/main/java/creator/loki/command/impl/entertainment/games/DiceCommand.java new file mode 100644 index 0000000..6478932 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/games/DiceCommand.java @@ -0,0 +1,60 @@ +package creator.loki.command.impl.entertainment.games; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; + +import java.awt.Color; +import java.util.Random; + +public class DiceCommand implements Command { + private final Random random = new Random(); + + @Override + public String getName() { + return "dice"; + } + + @Override + public String getDescription() { + return "Бросить кубик"; + } + + @Override + public String getUsage() { + return "dice [N]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + int maxValue = 6; + + if (!context.getArgs().isEmpty()) { + try { + maxValue = Integer.parseInt(context.getArgs().get(0)); + if (maxValue < 2 || maxValue > 100) { + context.getChannel().sendMessage("Число должно быть от 2 до 100").queue(); + return; + } + } catch (NumberFormatException e) { + context.getChannel().sendMessage("Укажи корректное число").queue(); + return; + } + } + + int result = random.nextInt(maxValue) + 1; + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("🎲 Бросаю кубик...") + .setDescription("**Результат:** " + result + " (из " + maxValue + ")") + .setColor(new Color(46, 204, 113)) + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/games/EightBallCommand.java b/src/main/java/creator/loki/command/impl/entertainment/games/EightBallCommand.java new file mode 100644 index 0000000..5d0e9ee --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/games/EightBallCommand.java @@ -0,0 +1,74 @@ +package creator.loki.command.impl.entertainment.games; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; + +import java.awt.Color; +import java.util.Random; + +public class EightBallCommand implements Command { + private final Random random = new Random(); + + private static final String[] ANSWERS = { + "Да, определённо", + "Бесспорно", + "Без сомнений", + "Можешь быть уверен", + "Мне кажется — да", + "Вероятнее всего", + "Хорошие перспективы", + "Знаки говорят — да", + "Да", + "Пока не ясно, попробуй снова", + "Спроси позже", + "Лучше не рассказывать", + "Сейчас нельзя предсказать", + "Сконцентрируйся и спроси опять", + "Даже не думай", + "Мой ответ — нет", + "По моим данным — нет", + "Перспективы не очень хорошие", + "Весьма сомнительно" + }; + + @Override + public String getName() { + return "8ball"; + } + + @Override + public String getDescription() { + return "Магический шар предсказаний"; + } + + @Override + public String getUsage() { + return "8ball <вопрос>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (context.getArgs().isEmpty()) { + context.getChannel().sendMessage("Задай вопрос!").queue(); + return; + } + + String question = String.join(" ", context.getArgs()); + String answer = ANSWERS[random.nextInt(ANSWERS.length)]; + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("🎱 Магический шар") + .addField("Вопрос:", question, false) + .addField("Ответ:", answer, false) + .setColor(new Color(52, 73, 94)) + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/games/FatCommand.java b/src/main/java/creator/loki/command/impl/entertainment/games/FatCommand.java new file mode 100644 index 0000000..a17339b --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/games/FatCommand.java @@ -0,0 +1,114 @@ +package creator.loki.command.impl.entertainment.games; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; +import java.util.Random; + +/** + * Fat метр - проверка на жирность + */ +public class FatCommand implements Command { + + private final CooldownManager cooldownManager; + + public FatCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "fat"; + } + + @Override + public String getDescription() { + return "Проверка на жирность (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "fat [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + // Проверка cooldown (5 секунд) + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "fat")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "fat"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); + + if (target == null) return; + + // Применяем cooldown + cooldownManager.applyCooldown(context.getAuthor().getId(), "fat", 5); + + // Детерминированный процент от 0 до 100 + int fatLevel = getDeterministicValue(target.getId(), 999, 0, 100); + + // Прогресс-бар + StringBuilder bar = new StringBuilder(); + int filled = fatLevel / 10; + for (int i = 0; i < filled; i++) { + bar.append("🟩"); + } + for (int i = filled; i < 10; i++) { + bar.append("⬜"); + } + + // Комментарий + String comment; + Color color; + if (fatLevel >= 90) { + comment = "🐷 СВИНОТА!"; + color = new Color(255, 0, 0); + } else if (fatLevel >= 70) { + comment = "🍔 Очень жирный"; + color = new Color(255, 100, 0); + } else if (fatLevel >= 50) { + comment = "🍕 Жирноват"; + color = new Color(255, 165, 0); + } else if (fatLevel >= 30) { + comment = "👍 Нормальный вес"; + color = new Color(0, 200, 0); + } else if (fatLevel >= 10) { + comment = "🥗 Худой"; + color = new Color(100, 200, 255); + } else { + comment = "💀 Очень худой"; + color = new Color(150, 150, 150); + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("🐷 Fat Метр") + .setDescription(target.getAsMention()) + .addField("Жирность", bar.toString() + "\n**" + fatLevel + "%** - " + comment, false) + .setThumbnail(target.getUser().getAvatarUrl()) + .setColor(color) + .setFooter("💡 Детерминировано вашим Discord ID") + .build() + ).queue(); + } + + private int getDeterministicValue(String userId, int salt, int min, int max) { + long seed = Long.parseLong(userId) + salt; + Random random = new Random(seed); + return random.nextInt(max - min + 1) + min; + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/games/GayCommand.java b/src/main/java/creator/loki/command/impl/entertainment/games/GayCommand.java new file mode 100644 index 0000000..38a0a34 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/games/GayCommand.java @@ -0,0 +1,97 @@ +package creator.loki.command.impl.entertainment.games; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; +import java.util.Random; + +/** + * Gay-метр с детерминированным результатом + */ +public class GayCommand implements Command { + + private final CooldownManager cooldownManager; + + public GayCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "gay"; + } + + @Override + public String getDescription() { + return "Гей-метр (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "gay [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + // Проверка cooldown (5 секунд) + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "gay")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "gay"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); + + if (target == null) return; + + if (target.getUser().isBot()) { + context.getChannel().sendMessage("❌ Боты не имеют ориентации!").queue(); + return; + } + + // Применяем cooldown + cooldownManager.applyCooldown(context.getAuthor().getId(), "gay", 5); + + // Детерминированный процент + int gayLevel = getDeterministicValue(target.getId(), 456, 0, 100); + + // Прогресс-бар с радугой + String[] rainbow = {"🟥", "🟧", "🟨", "🟩", "🟦", "🟪"}; + StringBuilder bar = new StringBuilder(); + int filled = Math.min(gayLevel / 17 + 1, 6); + for (int i = 0; i < filled; i++) { + bar.append(rainbow[i]); + } + for (int i = filled; i < 6; i++) { + bar.append("⬜"); + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("🏳️‍🌈 Гей-метр") + .setDescription(target.getAsMention()) + .addField("Уровень", bar.toString() + "\n**" + gayLevel + "%**", false) + .setThumbnail(target.getUser().getAvatarUrl()) + .setColor(new Color(255, 0, 255)) + .setFooter("💡 Это просто мем, не воспринимайте серьезно") + .build() + ).queue(); + } + + private int getDeterministicValue(String userId, int salt, int min, int max) { + long seed = Long.parseLong(userId) + salt; + Random random = new Random(seed); + return random.nextInt(max - min + 1) + min; + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/games/GlazbobaCommand.java b/src/main/java/creator/loki/command/impl/entertainment/games/GlazbobaCommand.java new file mode 100644 index 0000000..7d664a8 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/games/GlazbobaCommand.java @@ -0,0 +1,96 @@ +package creator.loki.command.impl.entertainment.games; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +public class GlazbobaCommand implements Command { + + @Override + public String getName() { + return "glazboba"; + } + + @Override + public String getDescription() { + return "Расширенная информация о пользователе"; + } + + @Override + public String getUsage() { + return "glazboba <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + // Анимация загрузки + context.getChannel().sendMessage("🔍 Сканирую данные...").queue(loadingMsg -> { + try { + Thread.sleep(1000); + loadingMsg.editMessage("📊 Анализирую профиль...").queue(); + Thread.sleep(1000); + loadingMsg.editMessage("🔐 Взламываю Discord...").queue(); + Thread.sleep(1000); + + // Собираем информацию + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); + String createdAt = target.getUser().getTimeCreated().format(formatter); + String joinedAt = target.getTimeJoined().format(formatter); + String status = target.getOnlineStatus().name(); + String nickname = target.getNickname() != null ? target.getNickname() : "Нет"; + String roles = target.getRoles().stream() + .map(role -> role.getName()) + .limit(10) + .collect(Collectors.joining(", ")); + if (roles.isEmpty()) roles = "Нет ролей"; + + String activities = target.getActivities().isEmpty() + ? "Нет активности" + : target.getActivities().get(0).getName(); + + boolean isBot = target.getUser().isBot(); + boolean isBoosting = target.isBoosting(); + + loadingMsg.editMessageEmbeds( + EmbedFactory.create() + .setTitle("👤 Информация о " + target.getEffectiveName()) + .setThumbnail(target.getUser().getAvatarUrl()) + .addField("🆔 ID", target.getId(), true) + .addField("📛 Никнейм", nickname, true) + .addField("🤖 Бот", isBot ? "Да" : "Нет", true) + .addField("📅 Аккаунт создан", createdAt, false) + .addField("📥 Присоединился", joinedAt, false) + .addField("🟢 Статус", status, true) + .addField("🎮 Активность", activities, true) + .addField("💎 Бустер", isBoosting ? "Да" : "Нет", true) + .addField("🎭 Роли", roles, false) + .setColor(target.getColor() != null ? target.getColor() : Color.BLUE) + .setFooter("Запросил: " + context.getAuthor().getUser().getName()) + .build() + ).queue(); + + } catch (InterruptedException e) { + loadingMsg.editMessage("❌ Ошибка загрузки").queue(); + } + }); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/games/IqCommand.java b/src/main/java/creator/loki/command/impl/entertainment/games/IqCommand.java new file mode 100644 index 0000000..7ee4533 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/games/IqCommand.java @@ -0,0 +1,107 @@ +package creator.loki.command.impl.entertainment.games; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; +import java.util.Random; + +/** + * IQ тест с детерминированным результатом + */ +public class IqCommand implements Command { + + private final CooldownManager cooldownManager; + + public IqCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "iq"; + } + + @Override + public String getDescription() { + return "Тест IQ (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "iq [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + // Проверка cooldown (5 секунд) + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "iq")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "iq"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); + + if (target == null) return; + + // Применяем cooldown + cooldownManager.applyCooldown(context.getAuthor().getId(), "iq", 5); + + int iq; + if (target.getUser().isBot()) { + iq = 9000; // Боты умные! + } else if (target.getId().equals("1414975197269987471")) { + iq = 1000; // Специальное значение + } else { + // Детерминированный IQ от 0 до 140 + iq = getDeterministicValue(target.getId(), 123, 0, 140); + } + + String level; + Color color; + if (iq >= 140) { + level = "🧠 Гений!"; + color = new Color(255, 215, 0); + } else if (iq >= 120) { + level = "🎓 Очень умный"; + color = new Color(100, 255, 150); + } else if (iq >= 100) { + level = "📚 Средний"; + color = new Color(52, 152, 219); + } else if (iq >= 80) { + level = "📖 Ниже среднего"; + color = new Color(255, 165, 0); + } else { + level = "🤔 Низкий"; + color = new Color(231, 76, 60); + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("🧠 Тест IQ") + .setDescription("Результат для " + target.getAsMention()) + .addField("IQ Уровень", "**" + iq + "** - " + level, false) + .setThumbnail(target.getUser().getAvatarUrl()) + .setColor(color) + .setFooter("💡 Научно доказано вашим ID") + .build() + ).queue(); + } + + private int getDeterministicValue(String userId, int salt, int min, int max) { + long seed = Long.parseLong(userId) + salt; + Random random = new Random(seed); + return random.nextInt(max - min + 1) + min; + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/games/MemeCommand.java b/src/main/java/creator/loki/command/impl/entertainment/games/MemeCommand.java new file mode 100644 index 0000000..8a98360 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/games/MemeCommand.java @@ -0,0 +1,73 @@ +package creator.loki.command.impl.entertainment.games; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; + +import java.awt.Color; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +public class MemeCommand implements Command { + + @Override + public String getName() { + return "meme"; + } + + @Override + public String getDescription() { + return "Случайный мем"; + } + + @Override + public String getUsage() { + return "meme"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + context.getChannel().sendMessage("🔍 Ищу мем...").queue(loadingMsg -> { + try { + URL url = new URL("https://meme-api.com/gimme"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("User-Agent", "Mozilla/5.0"); + + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + while ((line = in.readLine()) != null) { + response.append(line); + } + in.close(); + + JsonObject json = JsonParser.parseString(response.toString()).getAsJsonObject(); + String memeUrl = json.get("url").getAsString(); + String title = json.get("title").getAsString(); + String subreddit = json.get("subreddit").getAsString(); + + loadingMsg.editMessageEmbeds( + EmbedFactory.create() + .setTitle(title) + .setImage(memeUrl) + .setColor(new Color(255, 69, 0)) + .setFooter("r/" + subreddit) + .build() + ).queue(); + + } catch (Exception e) { + loadingMsg.editMessage("❌ Не удалось загрузить мем: " + e.getMessage()).queue(); + } + }); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/games/MustacheCommand.java b/src/main/java/creator/loki/command/impl/entertainment/games/MustacheCommand.java new file mode 100644 index 0000000..c84637e --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/games/MustacheCommand.java @@ -0,0 +1,126 @@ +package creator.loki.command.impl.entertainment.games; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; +import java.util.Random; + +/** + * Mustache метр - проверка на усатость + */ +public class MustacheCommand implements Command { + + private final CooldownManager cooldownManager; + + public MustacheCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "mustache"; + } + + @Override + public String getDescription() { + return "Проверка на усатость (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "mustache [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + // Проверка cooldown (5 секунд) + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "mustache")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "mustache"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); + + if (target == null) return; + + // Применяем cooldown + cooldownManager.applyCooldown(context.getAuthor().getId(), "mustache", 5); + + int mustacheLevel; + + // Специальные значения + if (target.getUser().isBot() || target.getId().equals("1414975197269987471")) { + mustacheLevel = 0; // Боты и специальный ID = 0% + } else if (target.getId().equals("678283217094180915")) { + mustacheLevel = 1000; // Специальный ID = 1000% + } else { + // Детерминированный процент от 0 до 100 + mustacheLevel = getDeterministicValue(target.getId(), 888, 0, 100); + } + + // Прогресс-бар с усами + StringBuilder bar = new StringBuilder(); + int filled = Math.min(mustacheLevel / 10, 10); + for (int i = 0; i < filled; i++) { + bar.append("🧔"); + } + for (int i = filled; i < 10; i++) { + bar.append("⬜"); + } + + // Комментарий + String comment; + Color color; + if (mustacheLevel >= 1000) { + comment = "🎩 ЛЕГЕНДАРНЫЕ УСИЩИ!"; + color = new Color(255, 215, 0); + } else if (mustacheLevel >= 80) { + comment = "🧔 Роскошные усы!"; + color = new Color(139, 69, 19); + } else if (mustacheLevel >= 60) { + comment = "👨 Хорошие усы"; + color = new Color(160, 82, 45); + } else if (mustacheLevel >= 40) { + comment = "😊 Усики растут"; + color = new Color(205, 133, 63); + } else if (mustacheLevel >= 20) { + comment = "🙂 Лёгкая щетина"; + color = new Color(210, 180, 140); + } else if (mustacheLevel > 0) { + comment = "😐 Почти нет усов"; + color = new Color(128, 128, 128); + } else { + comment = "🪒 Гладко выбрит"; + color = new Color(200, 200, 200); + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("🧔 Mustache Метр") + .setDescription(target.getAsMention()) + .addField("Усатость", bar.toString() + "\n**" + mustacheLevel + "%** - " + comment, false) + .setThumbnail(target.getUser().getAvatarUrl()) + .setColor(color) + .setFooter("💡 Детерминировано вашим Discord ID") + .build() + ).queue(); + } + + private int getDeterministicValue(String userId, int salt, int min, int max) { + long seed = Long.parseLong(userId) + salt; + Random random = new Random(seed); + return random.nextInt(max - min + 1) + min; + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/games/PpCommand.java b/src/main/java/creator/loki/command/impl/entertainment/games/PpCommand.java new file mode 100644 index 0000000..4879c6a --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/games/PpCommand.java @@ -0,0 +1,122 @@ +package creator.loki.command.impl.entertainment.games; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; +import java.util.Random; + +/** + * PP размер с детерминированным результатом + */ +public class PpCommand implements Command { + + private final CooldownManager cooldownManager; + + public PpCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "pp"; + } + + @Override + public String getDescription() { + return "Измеритель pp (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "pp [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + // Проверка cooldown (5 секунд) + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "pp")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "pp"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); + + if (target == null) return; + + if (target.getUser().isBot()) { + context.getChannel().sendMessage("❌ У ботов нет pp!").queue(); + return; + } + + // Применяем cooldown + cooldownManager.applyCooldown(context.getAuthor().getId(), "pp", 5); + + // Специальное значение для конкретного пользователя + int size; + if (target.getId().equals("1414975197269987471")) { + size = 100; + } else { + // Детерминированный размер от 0 до 30 см + size = getDeterministicValue(target.getId(), 6969, 0, 30); + } + + // Визуализация + StringBuilder ppVisual = new StringBuilder("8"); + for (int i = 0; i < size; i++) { + ppVisual.append("="); + } + ppVisual.append("D"); + + // Определяем уровень + String comment; + Color color; + if (size >= 25) { + comment = "😳 НЕВЕРОЯТНО!"; + color = new Color(255, 215, 0); + } else if (size >= 20) { + comment = "😮 Впечатляет"; + color = new Color(100, 255, 150); + } else if (size >= 15) { + comment = "👍 Выше среднего"; + color = new Color(52, 152, 219); + } else if (size >= 10) { + comment = "🙂 Средний"; + color = new Color(128, 128, 128); + } else if (size >= 5) { + comment = "😐 Ниже среднего"; + color = new Color(255, 165, 0); + } else { + comment = "💀 Микро"; + color = new Color(231, 76, 60); + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("🍆 PP Размер") + .setDescription(target.getAsMention()) + .addField("Размер", "```" + ppVisual.toString() + "```\n**" + size + " см** - " + comment, false) + .setThumbnail(target.getUser().getAvatarUrl()) + .setColor(color) + .setFooter("💡 Детерминировано вашим Discord ID") + .build() + ).queue(); + } + + private int getDeterministicValue(String userId, int salt, int min, int max) { + long seed = Long.parseLong(userId) + salt; + Random random = new Random(seed); + return random.nextInt(max - min + 1) + min; + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/games/RateCommand.java b/src/main/java/creator/loki/command/impl/entertainment/games/RateCommand.java new file mode 100644 index 0000000..9c4f008 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/games/RateCommand.java @@ -0,0 +1,107 @@ +package creator.loki.command.impl.entertainment.games; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * Оценка пользователя из 10 с детерминированным результатом + */ +public class RateCommand implements Command { + + private final CooldownManager cooldownManager; + + public RateCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "rate"; + } + + @Override + public String getDescription() { + return "Оценить пользователя из 10 (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "rate [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + // Проверка cooldown (5 секунд) + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "rate")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "rate"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); + + if (target == null) return; + + // Применяем cooldown + cooldownManager.applyCooldown(context.getAuthor().getId(), "rate", 5); + + // Детерминированная оценка от 1 до 10 + int rating = getDeterministicValue(target.getId(), 420, 1, 10); + + // Визуализация звездами + StringBuilder stars = new StringBuilder(); + for (int i = 0; i < rating; i++) { + stars.append("⭐"); + } + for (int i = rating; i < 10; i++) { + stars.append("☆"); + } + + // Комментарий + Map comments = new HashMap<>(); + comments.put(10, "💯 ИДЕАЛЬНО!"); + comments.put(9, "🔥 Почти идеально!"); + comments.put(8, "😍 Отлично!"); + comments.put(7, "👍 Хорошо!"); + comments.put(6, "🙂 Неплохо"); + comments.put(5, "😐 Средне"); + comments.put(4, "😕 Ниже среднего"); + comments.put(3, "😬 Так себе"); + comments.put(2, "💀 Плохо"); + comments.put(1, "🗿 Ужасно"); + + String comment = comments.getOrDefault(rating, "🤔"); + Color color = rating >= 8 ? new Color(255, 215, 0) : new Color(52, 152, 219); + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("⭐ Оценка") + .setDescription(target.getAsMention()) + .addField("Рейтинг", stars.toString() + "\n**" + rating + "/10** - " + comment, false) + .setThumbnail(target.getUser().getAvatarUrl()) + .setColor(color) + .build() + ).queue(); + } + + private int getDeterministicValue(String userId, int salt, int min, int max) { + long seed = Long.parseLong(userId) + salt; + Random random = new Random(seed); + return random.nextInt(max - min + 1) + min; + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/interaction/ComplimentCommand.java b/src/main/java/creator/loki/command/impl/entertainment/interaction/ComplimentCommand.java new file mode 100644 index 0000000..4e37cae --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/interaction/ComplimentCommand.java @@ -0,0 +1,67 @@ +package creator.loki.command.impl.entertainment.interaction; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; +import java.util.Random; + +public class ComplimentCommand implements Command { + private final Random random = new Random(); + + private static final String[] COMPLIMENTS = { + "Ты как идеальный код — красивый и без багов", + "Твоя улыбка ярче, чем RGB подсветка", + "Ты как хороший коммит — делаешь всё лучше", + "Твой вайб как у Python — простой и приятный", + "Ты как успешный деплой — всё работает отлично", + "Твоя энергия как у SSD — быстрая и эффективная", + "Ты как open source проект — полезный для всех", + "Твоя харизма как у TypeScript — добавляет уверенности", + "Ты как хороший API — понятный и надёжный", + "Твоё присутствие как кофе для программиста — необходимо" + }; + + @Override + public String getName() { + return "compliment"; + } + + @Override + public String getDescription() { + return "Сделать комплимент пользователю"; + } + + @Override + public String getUsage() { + return "compliment <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + String compliment = COMPLIMENTS[random.nextInt(COMPLIMENTS.length)]; + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("💖 Комплимент") + .setDescription(target.getAsMention() + ", " + compliment) + .setColor(new Color(255, 182, 193)) + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/interaction/CuddleCommand.java b/src/main/java/creator/loki/command/impl/entertainment/interaction/CuddleCommand.java new file mode 100644 index 0000000..de826b2 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/interaction/CuddleCommand.java @@ -0,0 +1,78 @@ +package creator.loki.command.impl.entertainment.interaction; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import creator.loki.util.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +/** + * Обниматься с пользователем с GIF + */ +public class CuddleCommand implements Command { + + private final CooldownManager cooldownManager; + + public CuddleCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "cuddle"; + } + + @Override + public String getDescription() { + return "Обниматься с пользователем"; + } + + @Override + public String getUsage() { + return "cuddle <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "cuddle")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "cuddle"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + cooldownManager.applyCooldown(context.getAuthor().getId(), "cuddle", 5); + + String gifUrl = NekosLifeApi.fetchImage("cuddle"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("🥰 " + context.getAuthor().getAsMention() + " обнимается с " + target.getAsMention() + "! 💕") + .setImage(gifUrl) + .setColor(new Color(255, 0, 255)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/interaction/FeedCommand.java b/src/main/java/creator/loki/command/impl/entertainment/interaction/FeedCommand.java new file mode 100644 index 0000000..0f8ded1 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/interaction/FeedCommand.java @@ -0,0 +1,78 @@ +package creator.loki.command.impl.entertainment.interaction; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import creator.loki.util.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +/** + * Покормить пользователя с GIF + */ +public class FeedCommand implements Command { + + private final CooldownManager cooldownManager; + + public FeedCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "feed"; + } + + @Override + public String getDescription() { + return "Покормить пользователя"; + } + + @Override + public String getUsage() { + return "feed <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "feed")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "feed"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + cooldownManager.applyCooldown(context.getAuthor().getId(), "feed", 5); + + String gifUrl = NekosLifeApi.fetchImage("feed"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("🍽️ " + context.getAuthor().getAsMention() + " кормит " + target.getAsMention() + "! 😋") + .setImage(gifUrl) + .setColor(new Color(255, 200, 100)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/interaction/HugCommand.java b/src/main/java/creator/loki/command/impl/entertainment/interaction/HugCommand.java new file mode 100644 index 0000000..26d9d2a --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/interaction/HugCommand.java @@ -0,0 +1,61 @@ +package creator.loki.command.impl.entertainment.interaction; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +public class HugCommand implements Command { + + @Override + public String getName() { + return "hug"; + } + + @Override + public String getDescription() { + return "Обнять пользователя"; + } + + @Override + public String getUsage() { + return "hug <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + // Получаем GIF из nekos.life API + String gifUrl = NekosLifeApi.fetchImage("hug"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("🤗 " + context.getAuthor().getAsMention() + " обнял " + target.getAsMention() + " 💕") + .setImage(gifUrl) + .setColor(new Color(255, 182, 193)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/interaction/KissCommand.java b/src/main/java/creator/loki/command/impl/entertainment/interaction/KissCommand.java new file mode 100644 index 0000000..19fab28 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/interaction/KissCommand.java @@ -0,0 +1,92 @@ +package creator.loki.command.impl.entertainment.interaction; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import creator.loki.util.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +/** + * Поцелуй пользователя с GIF + */ +public class KissCommand implements Command { + + private final CooldownManager cooldownManager; + + public KissCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "kiss"; + } + + @Override + public String getDescription() { + return "Поцеловать пользователя"; + } + + @Override + public String getUsage() { + return "kiss <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + // Проверка cooldown (5 секунд) + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "kiss")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "kiss"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи кого поцеловать! Использование: `!kiss @user`").queue(); + return; + } + + Member target = mentioned.get(0); + + if (target.getId().equals(context.getAuthor().getId())) { + context.getChannel().sendMessage("❌ Нельзя целовать самого себя!").queue(); + return; + } + + if (target.getUser().isBot()) { + context.getChannel().sendMessage("❌ Боты не умеют целоваться!").queue(); + return; + } + + // Применяем cooldown + cooldownManager.applyCooldown(context.getAuthor().getId(), "kiss", 5); + + // Получаем GIF из nekos.life API + String gifUrl = NekosLifeApi.fetchImage("kiss"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("💋 " + context.getAuthor().getAsMention() + " целует " + target.getAsMention() + " ❤️") + .setImage(gifUrl) + .setColor(new Color(255, 105, 180)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/interaction/PatCommand.java b/src/main/java/creator/loki/command/impl/entertainment/interaction/PatCommand.java new file mode 100644 index 0000000..4495969 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/interaction/PatCommand.java @@ -0,0 +1,91 @@ +package creator.loki.command.impl.entertainment.interaction; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.util.CooldownManager; +import net.dv8tion.jda.api.entities.Member; + +import java.util.List; + +public class PatCommand implements Command { + private static final int COOLDOWN_SECONDS = 5; + private final CooldownManager cooldownManager; + + public PatCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "pat"; + } + + @Override + public String getDescription() { + return "Погладить пользователя"; + } + + @Override + public String getUsage() { + return "pat <@user> <количество 1-7>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + String userId = context.getAuthor().getId(); + + // Проверка cooldown + if (cooldownManager.isOnCooldown(userId, "pat")) { + long remaining = cooldownManager.getRemainingSeconds(userId, "pat"); + context.getChannel().sendMessage( + "⏰ Подожди " + remaining + " сек." + ).queue(); + return; + } + + // Проверка аргументов + if (context.getArgs().size() < 2) { + context.getChannel().sendMessage( + "❌ Используй: `!pat <@user> <количество 1-7>`" + ).queue(); + return; + } + + // Получение упомянутого пользователя + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + // Парсинг количества + int count; + try { + count = Integer.parseInt(context.getArgs().get(1)); + } catch (NumberFormatException e) { + context.getChannel().sendMessage("❌ Укажи число от 1 до 7").queue(); + return; + } + + // Валидация количества + if (count < 1 || count > 7) { + context.getChannel().sendMessage("❌ Количество должно быть от 1 до 7").queue(); + return; + } + + // Применяем cooldown + cooldownManager.applyCooldown(userId, "pat", COOLDOWN_SECONDS); + + // Отправляем каждое "pat-pat" отдельным сообщением + for (int i = 0; i < count; i++) { + context.getChannel().sendMessage("pat-pat " + target.getAsMention()).queue(); + } + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/interaction/PokeCommand.java b/src/main/java/creator/loki/command/impl/entertainment/interaction/PokeCommand.java new file mode 100644 index 0000000..bfe7154 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/interaction/PokeCommand.java @@ -0,0 +1,78 @@ +package creator.loki.command.impl.entertainment.interaction; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import creator.loki.util.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +/** + * Тыкнуть пользователя с GIF + */ +public class PokeCommand implements Command { + + private final CooldownManager cooldownManager; + + public PokeCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "poke"; + } + + @Override + public String getDescription() { + return "Тыкнуть пользователя"; + } + + @Override + public String getUsage() { + return "poke <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "poke")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "poke"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + cooldownManager.applyCooldown(context.getAuthor().getId(), "poke", 5); + + String gifUrl = NekosLifeApi.fetchImage("poke"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("👉 " + context.getAuthor().getAsMention() + " тыкает в " + target.getAsMention() + "! 😏") + .setImage(gifUrl) + .setColor(new Color(0, 128, 128)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/interaction/RoastCommand.java b/src/main/java/creator/loki/command/impl/entertainment/interaction/RoastCommand.java new file mode 100644 index 0000000..b15d025 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/interaction/RoastCommand.java @@ -0,0 +1,68 @@ +package creator.loki.command.impl.entertainment.interaction; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; +import java.util.Random; + +public class RoastCommand implements Command { + private final Random random = new Random(); + + private static final String[] ROASTS = { + "Ты как Internet Explorer — медленный и никому не нужный", + "Твой код работает? Это баг, исправь его", + "Ты пишешь код как будто тебе за каждую строку платят", + "Твоя логика как у JavaScript: непредсказуемая", + "Ты как HTML без CSS — работаешь, но выглядишь ужасно", + "Твой IQ как температура в Антарктиде — ниже нуля", + "Ты как баг в продакшене — неожиданный и нежеланный", + "Твои шутки как deprecated код — устарели", + "Ты как NULL pointer — бесполезный и вызываешь ошибки", + "Твой уровень как индекс массива в Python — может быть отрицательным" + }; + + @Override + public String getName() { + return "roast"; + } + + @Override + public String getDescription() { + return "Дружеский рофл над пользователем"; + } + + @Override + public String getUsage() { + return "roast <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + String roast = ROASTS[random.nextInt(ROASTS.length)]; + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("🔥 Roast") + .setDescription(target.getAsMention() + ", " + roast) + .setColor(new Color(231, 76, 60)) + .setFooter("Это шутка, не обижайся!") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/interaction/ShipCommand.java b/src/main/java/creator/loki/command/impl/entertainment/interaction/ShipCommand.java new file mode 100644 index 0000000..ba59d37 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/interaction/ShipCommand.java @@ -0,0 +1,120 @@ +package creator.loki.command.impl.entertainment.interaction; + +import com.google.gson.reflect.TypeToken; +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.data.JsonDataStore; +import creator.loki.embed.EmbedFactory; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ShipCommand implements Command { + private final JsonDataStore dataStore; + + public ShipCommand(JsonDataStore dataStore) { + this.dataStore = dataStore; + } + + @Override + public String getName() { + return "ship"; + } + + @Override + public String getDescription() { + return "Процент совместимости двух пользователей"; + } + + @Override + public String getUsage() { + return "ship <@user1> <@user2>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.size() < 2) { + context.getChannel().sendMessage("Укажи двух пользователей через @").queue(); + return; + } + + Member user1 = mentioned.get(0); + Member user2 = mentioned.get(1); + + // Создаём уникальный ключ для пары (сортируем ID чтобы порядок не имел значения) + String pairKey = createPairKey(user1.getId(), user2.getId()); + + // Получаем или создаём процент + Type type = new TypeToken>(){}.getType(); + Map shipPairs = dataStore.getData("shipPairs", type); + if (shipPairs == null) { + shipPairs = new HashMap<>(); + } + + int percentage; + if (shipPairs.containsKey(pairKey)) { + percentage = shipPairs.get(pairKey); + } else { + // Генерируем процент на основе хеша ID (чтобы был стабильный) + percentage = Math.abs((user1.getId() + user2.getId()).hashCode()) % 101; + shipPairs.put(pairKey, percentage); + dataStore.setData("shipPairs", shipPairs); + } + + // Генерируем сердечки + String hearts = generateHearts(percentage); + String message = generateMessage(percentage); + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("💕 Ship-метр") + .setDescription( + user1.getAsMention() + " 💖 " + user2.getAsMention() + "\n\n" + + hearts + " **" + percentage + "%**\n\n" + + message + ) + .setColor(getColorByPercentage(percentage)) + .build() + ).queue(); + } + + private String createPairKey(String id1, String id2) { + return id1.compareTo(id2) < 0 ? id1 + "-" + id2 : id2 + "-" + id1; + } + + private String generateHearts(int percentage) { + int fullHearts = percentage / 10; + StringBuilder hearts = new StringBuilder(); + for (int i = 0; i < fullHearts; i++) { + hearts.append("❤️"); + } + for (int i = fullHearts; i < 10; i++) { + hearts.append("🖤"); + } + return hearts.toString(); + } + + private String generateMessage(int percentage) { + if (percentage >= 90) return "Идеальная пара! 💑"; + if (percentage >= 70) return "Отличная совместимость! 💕"; + if (percentage >= 50) return "Неплохие шансы! 💖"; + if (percentage >= 30) return "Есть потенциал... 💔"; + return "Лучше остаться друзьями 💀"; + } + + private Color getColorByPercentage(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); // Серый + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/interaction/SlapCommand.java b/src/main/java/creator/loki/command/impl/entertainment/interaction/SlapCommand.java new file mode 100644 index 0000000..7caf256 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/interaction/SlapCommand.java @@ -0,0 +1,78 @@ +package creator.loki.command.impl.entertainment.interaction; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import creator.loki.util.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +/** + * Шлепнуть пользователя с GIF + */ +public class SlapCommand implements Command { + + private final CooldownManager cooldownManager; + + public SlapCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "slap"; + } + + @Override + public String getDescription() { + return "Шлепнуть пользователя"; + } + + @Override + public String getUsage() { + return "slap <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "slap")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "slap"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + cooldownManager.applyCooldown(context.getAuthor().getId(), "slap", 5); + + String gifUrl = NekosLifeApi.fetchImage("slap"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("👋 " + context.getAuthor().getAsMention() + " шлепает " + target.getAsMention() + "! 😠") + .setImage(gifUrl) + .setColor(new Color(255, 165, 0)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/entertainment/interaction/TickleCommand.java b/src/main/java/creator/loki/command/impl/entertainment/interaction/TickleCommand.java new file mode 100644 index 0000000..1eee42b --- /dev/null +++ b/src/main/java/creator/loki/command/impl/entertainment/interaction/TickleCommand.java @@ -0,0 +1,78 @@ +package creator.loki.command.impl.entertainment.interaction; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.util.CooldownManager; +import creator.loki.util.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +/** + * Пощекотать пользователя с GIF + */ +public class TickleCommand implements Command { + + private final CooldownManager cooldownManager; + + public TickleCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "tickle"; + } + + @Override + public String getDescription() { + return "Пощекотать пользователя"; + } + + @Override + public String getUsage() { + return "tickle <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "tickle")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "tickle"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + cooldownManager.applyCooldown(context.getAuthor().getId(), "tickle", 5); + + String gifUrl = NekosLifeApi.fetchImage("tickle"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("🤗 " + context.getAuthor().getAsMention() + " щекочет " + target.getAsMention() + "!") + .setImage(gifUrl) + .setColor(new Color(0, 255, 0)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/moderation/BanCommand.java b/src/main/java/creator/loki/command/impl/moderation/BanCommand.java new file mode 100644 index 0000000..df3c95e --- /dev/null +++ b/src/main/java/creator/loki/command/impl/moderation/BanCommand.java @@ -0,0 +1,126 @@ +package creator.loki.command.impl.moderation; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedTemplates; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Member; + +import java.util.List; + +public class BanCommand implements Command { + + @Override + public String getName() { + return "ban"; + } + + @Override + public String getDescription() { + return "Банит пользователя на сервере"; + } + + @Override + public String getUsage() { + return "ban <@user> [причина]"; + } + + @Override + public String getCategory() { + return "Модерация"; + } + + @Override + public void execute(CommandContext context) { + if (context.getGuild() == null) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Команда доступна только на сервере") + ).queue(); + return; + } + + // Проверка прав модератора + if (context.getAuthor() == null || !context.getAuthor().hasPermission(Permission.BAN_MEMBERS)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "У вас нет прав на бан пользователей") + ).queue(); + return; + } + + // Проверка прав бота + if (!context.getGuild().getSelfMember().hasPermission(Permission.BAN_MEMBERS)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "У бота нет прав на бан пользователей") + ).queue(); + return; + } + + // Получение упомянутого пользователя + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Укажите пользователя через @") + ).queue(); + return; + } + + Member target = mentioned.get(0); + + // Нельзя забанить самого себя + if (target.getId().equals(context.getAuthor().getId())) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Вы не можете забанить самого себя") + ).queue(); + return; + } + + // Нельзя забанить бота + if (target.getUser().isBot() && target.getId().equals(context.getGuild().getSelfMember().getId())) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Я не могу забанить самого себя") + ).queue(); + return; + } + + // Проверка иерархии ролей + if (!context.getAuthor().canInteract(target)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "Вы не можете забанить этого пользователя (роль выше вашей)") + ).queue(); + return; + } + + if (!context.getGuild().getSelfMember().canInteract(target)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "Я не могу забанить этого пользователя (роль выше моей)") + ).queue(); + return; + } + + // Причина бана + String reason = context.getArgs().size() > 1 + ? String.join(" ", context.getArgs().subList(1, context.getArgs().size())) + : "Причина не указана"; + + // Бан + context.getGuild().ban(target, 0, java.util.concurrent.TimeUnit.DAYS) + .reason(reason) + .queue( + success -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.success( + "Пользователь забанен", + "**Пользователь:** " + target.getUser().getAsTag() + "\n" + + "**Причина:** " + reason + "\n" + + "**Модератор:** " + context.getAuthor().getAsMention() + ) + ).queue(); + }, + error -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Не удалось забанить: " + error.getMessage()) + ).queue(); + } + ); + } +} diff --git a/src/main/java/creator/loki/command/impl/moderation/ClearCommand.java b/src/main/java/creator/loki/command/impl/moderation/ClearCommand.java new file mode 100644 index 0000000..71cd2df --- /dev/null +++ b/src/main/java/creator/loki/command/impl/moderation/ClearCommand.java @@ -0,0 +1,103 @@ +package creator.loki.command.impl.moderation; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedTemplates; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; + +import java.util.concurrent.TimeUnit; + +public class ClearCommand implements Command { + + @Override + public String getName() { + return "clear"; + } + + @Override + public String getDescription() { + return "Удаляет указанное количество сообщений"; + } + + @Override + public String getUsage() { + return "clear <количество>"; + } + + @Override + public void execute(CommandContext context) { + // Проверка прав + if (context.getAuthor() == null || !context.getAuthor().hasPermission(Permission.MESSAGE_MANAGE)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Недостаточно прав", + "У вас нет прав на управление сообщениями" + ) + ).queue(); + return; + } + + // Проверка аргументов + if (context.getArgs().isEmpty()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Неверное количество", + "Укажите число от 1 до 100" + ) + ).queue(); + return; + } + + int amount; + try { + amount = Integer.parseInt(context.getArgs().get(0)); + } catch (NumberFormatException e) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Неверное количество", + "Укажите число от 1 до 100" + ) + ).queue(); + return; + } + + if (amount < 1 || amount > 100) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Неверное количество", + "Укажите число от 1 до 100" + ) + ).queue(); + return; + } + + // Удаление сообщений + if (!(context.getChannel() instanceof TextChannel)) { + return; + } + + TextChannel channel = (TextChannel) context.getChannel(); + + channel.getHistory().retrievePast(amount + 1).queue(messages -> { + channel.deleteMessages(messages).queue( + success -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.success( + "Сообщения удалены", + "Удалено сообщений: " + messages.size() + ) + ).queue(msg -> msg.delete().queueAfter(5, TimeUnit.SECONDS)); + }, + error -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Ошибка удаления", + "Не удалось удалить сообщения: " + error.getMessage() + ) + ).queue(); + } + ); + }); + } +} diff --git a/src/main/java/creator/loki/command/impl/moderation/KickCommand.java b/src/main/java/creator/loki/command/impl/moderation/KickCommand.java new file mode 100644 index 0000000..4ad1d01 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/moderation/KickCommand.java @@ -0,0 +1,118 @@ +package creator.loki.command.impl.moderation; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedTemplates; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Member; + +import java.util.List; + +public class KickCommand implements Command { + + @Override + public String getName() { + return "kick"; + } + + @Override + public String getDescription() { + return "Кикает пользователя с сервера"; + } + + @Override + public String getUsage() { + return "kick <@user> [причина]"; + } + + @Override + public String getCategory() { + return "Модерация"; + } + + @Override + public void execute(CommandContext context) { + if (context.getGuild() == null) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Команда доступна только на сервере") + ).queue(); + return; + } + + if (context.getAuthor() == null || !context.getAuthor().hasPermission(Permission.KICK_MEMBERS)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "У вас нет прав на кик пользователей") + ).queue(); + return; + } + + if (!context.getGuild().getSelfMember().hasPermission(Permission.KICK_MEMBERS)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "У бота нет прав на кик пользователей") + ).queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Укажите пользователя через @") + ).queue(); + return; + } + + Member target = mentioned.get(0); + + if (target.getId().equals(context.getAuthor().getId())) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Вы не можете кикнуть самого себя") + ).queue(); + return; + } + + if (target.getUser().isBot() && target.getId().equals(context.getGuild().getSelfMember().getId())) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Я не могу кикнуть самого себя") + ).queue(); + return; + } + + if (!context.getAuthor().canInteract(target)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "Вы не можете кикнуть этого пользователя") + ).queue(); + return; + } + + if (!context.getGuild().getSelfMember().canInteract(target)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "Я не могу кикнуть этого пользователя") + ).queue(); + return; + } + + String reason = context.getArgs().size() > 1 + ? String.join(" ", context.getArgs().subList(1, context.getArgs().size())) + : "Причина не указана"; + + context.getGuild().kick(target) + .reason(reason) + .queue( + success -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.success( + "Пользователь кикнут", + "**Пользователь:** " + target.getUser().getAsTag() + "\n" + + "**Причина:** " + reason + "\n" + + "**Модератор:** " + context.getAuthor().getAsMention() + ) + ).queue(); + }, + error -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Не удалось кикнуть: " + error.getMessage()) + ).queue(); + } + ); + } +} diff --git a/src/main/java/creator/loki/command/impl/roles/CreateRoleCommand.java b/src/main/java/creator/loki/command/impl/roles/CreateRoleCommand.java new file mode 100644 index 0000000..1ac41aa --- /dev/null +++ b/src/main/java/creator/loki/command/impl/roles/CreateRoleCommand.java @@ -0,0 +1,199 @@ +package creator.loki.command.impl.roles; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedTemplates; +import creator.loki.role.CustomRoleManager; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Role; + +import java.awt.Color; +import java.util.regex.Pattern; + +public class CreateRoleCommand implements Command { + private static final Pattern HEX_PATTERN = Pattern.compile("^#?[0-9A-Fa-f]{6}$"); + private final CustomRoleManager roleManager; + + public CreateRoleCommand(CustomRoleManager roleManager) { + this.roleManager = roleManager; + } + + @Override + public String getName() { + return "createrole"; + } + + @Override + public String getDescription() { + return "Создаёт кастомную роль с указанным цветом"; + } + + @Override + public String getUsage() { + return "createrole <название> <#HEX цвет>"; + } + + @Override + public String getCategory() { + return "Роли"; + } + + @Override + public void execute(CommandContext context) { + if (!RoleCommandValidator.validateGuild(context)) return; + + Guild guild = context.getGuild(); + if (!RoleCommandValidator.validateBotPermissions(guild, context)) return; + if (!RoleCommandValidator.validateArgumentCount(context, 2, + "Используй: `!createrole <название> <#цвет>`\nПример: `!createrole Cool Role #FF5733`")) return; + + RoleData roleData = parseRoleData(context); + if (!validateRoleName(roleData.name, guild, context)) return; + + Color color = parseColor(roleData.colorArg, context); + if (color == null) return; + + if (!checkUserLimit(guild, context)) return; + + createAndAssignRole(guild, context, roleData.name, color); + } + + private boolean validatePermissions(Guild guild, CommandContext context) { + if (!guild.getSelfMember().hasPermission(Permission.MANAGE_ROLES)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "У бота нет прав на управление ролями") + ).queue(); + return false; + } + return true; + } + + private boolean validateArguments(CommandContext context) { + if (context.getArgs().size() < 2) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Неверное использование", + "Используй: `!createrole <название> <#цвет>`\nПример: `!createrole Cool Role #FF5733`") + ).queue(); + return false; + } + return true; + } + + private RoleData parseRoleData(CommandContext context) { + String colorArg = context.getArgs().get(context.getArgs().size() - 1); + String name = String.join(" ", context.getArgs().subList(0, context.getArgs().size() - 1)); + return new RoleData(name, colorArg); + } + + private boolean validateRoleName(String name, Guild guild, CommandContext context) { + if (!RoleCommandValidator.validateRoleName(name, context)) { + return false; + } + + if (!guild.getRolesByName(name, true).isEmpty()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Роль уже существует", "Роль с названием `" + name + "` уже существует на сервере") + ).queue(); + return false; + } + + return true; + } + + private Color parseColor(String colorArg, CommandContext context) { + if (!HEX_PATTERN.matcher(colorArg).matches()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Неверный цвет", "Укажите цвет в HEX формате (например: #FF5733)") + ).queue(); + return null; + } + + try { + String hex = colorArg.startsWith("#") ? colorArg : "#" + colorArg; + return Color.decode(hex); + } catch (NumberFormatException e) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Неверный цвет", "Не удалось распознать цвет: " + colorArg) + ).queue(); + return null; + } + } + + private boolean checkUserLimit(Guild guild, CommandContext context) { + String userId = context.getAuthor().getId(); + String guildId = guild.getId(); + + if (roleManager.hasReachedLimit(guildId, userId)) { + int current = roleManager.getUserRoleCount(guildId, userId); + int max = roleManager.getMaxRolesPerUser(); + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Лимит достигнут", + "Вы достигли максимального количества кастомных ролей (" + current + "/" + max + ")\n" + + "Удалите старую роль командой `!deleterole <название>`" + ) + ).queue(); + return false; + } + return true; + } + + private void createAndAssignRole(Guild guild, CommandContext context, String name, Color color) { + guild.createRole() + .setName(name) + .setColor(color) + .setMentionable(true) + .queue( + role -> assignRoleToUser(guild, context, role), + error -> sendCreationError(context, error) + ); + } + + private void assignRoleToUser(Guild guild, CommandContext context, Role role) { + guild.addRoleToMember(context.getAuthor(), role).queue( + success -> onRoleAssigned(guild, context, role), + error -> sendAssignmentError(context, error) + ); + } + + private void onRoleAssigned(Guild guild, CommandContext context, Role role) { + String userId = context.getAuthor().getId(); + String guildId = guild.getId(); + + roleManager.trackRole(guildId, userId, role.getId()); + + int current = roleManager.getUserRoleCount(guildId, userId); + int max = roleManager.getMaxRolesPerUser(); + + context.getChannel().sendMessageEmbeds( + EmbedTemplates.success( + "Роль создана", + "Роль " + role.getAsMention() + " создана и назначена вам!\n" + + "Ваши кастомные роли: " + current + "/" + max + ) + ).queue(); + } + + private void sendAssignmentError(CommandContext context, Throwable error) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Роль создана, но не удалось назначить её вам: " + error.getMessage()) + ).queue(); + } + + private void sendCreationError(CommandContext context, Throwable error) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка создания", "Не удалось создать роль: " + error.getMessage()) + ).queue(); + } + + private static class RoleData { + final String name; + final String colorArg; + + RoleData(String name, String colorArg) { + this.name = name; + this.colorArg = colorArg; + } + } +} diff --git a/src/main/java/creator/loki/command/impl/roles/DeleteRoleCommand.java b/src/main/java/creator/loki/command/impl/roles/DeleteRoleCommand.java new file mode 100644 index 0000000..b15d992 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/roles/DeleteRoleCommand.java @@ -0,0 +1,132 @@ +package creator.loki.command.impl.roles; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedTemplates; +import creator.loki.role.CustomRoleManager; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Role; + +import java.util.List; + +public class DeleteRoleCommand implements Command { + private final CustomRoleManager roleManager; + + public DeleteRoleCommand(CustomRoleManager roleManager) { + this.roleManager = roleManager; + } + + @Override + public String getName() { + return "deleterole"; + } + + @Override + public String getDescription() { + return "Удаляет вашу кастомную роль"; + } + + @Override + public String getUsage() { + return "deleterole <название>"; + } + + @Override + public String getCategory() { + return "Роли"; + } + + @Override + public void execute(CommandContext context) { + Guild guild = context.getGuild(); + if (guild == null) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Команда доступна только на сервере") + ).queue(); + return; + } + + // Проверка прав бота + if (!guild.getSelfMember().hasPermission(Permission.MANAGE_ROLES)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "У бота нет прав на управление ролями") + ).queue(); + return; + } + + // Проверка аргументов + if (context.getArgs().isEmpty()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Неверное использование", "Используй: `!deleterole <название>`") + ).queue(); + return; + } + + // Название роли + String roleName = String.join(" ", context.getArgs()); + + // Поиск роли + List roles = guild.getRolesByName(roleName, true); + if (roles.isEmpty()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Роль не найдена", "Роль с названием `" + roleName + "` не найдена на сервере") + ).queue(); + return; + } + + Role role = roles.get(0); + String userId = context.getAuthor().getId(); + String guildId = guild.getId(); + + // Проверка владения + if (!roleManager.isOwnedBy(guildId, userId, role.getId())) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Нет прав", + "Вы можете удалять только свои кастомные роли\n" + + "Используй `!myroles` чтобы посмотреть список своих ролей" + ) + ).queue(); + return; + } + + // Проверка позиции (бот может удалить роль) + if (!guild.getSelfMember().canInteract(role)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Невозможно удалить", + "Роль находится выше роли бота в иерархии. Переместите роль бота выше в настройках сервера." + ) + ).queue(); + return; + } + + // Удаление роли + String roleId = role.getId(); + String mention = role.getAsMention(); + + role.delete().queue( + success -> { + // Удаляем из отслеживания + roleManager.untrackRole(guildId, userId, roleId); + + int remaining = roleManager.getUserRoleCount(guildId, userId); + int max = roleManager.getMaxRolesPerUser(); + + context.getChannel().sendMessageEmbeds( + EmbedTemplates.success( + "Роль удалена", + "Роль " + mention + " успешно удалена\n" + + "Ваши кастомные роли: " + remaining + "/" + max + ) + ).queue(); + }, + error -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка удаления", "Не удалось удалить роль: " + error.getMessage()) + ).queue(); + } + ); + } +} diff --git a/src/main/java/creator/loki/command/impl/roles/EditRoleCommand.java b/src/main/java/creator/loki/command/impl/roles/EditRoleCommand.java new file mode 100644 index 0000000..ed0cedf --- /dev/null +++ b/src/main/java/creator/loki/command/impl/roles/EditRoleCommand.java @@ -0,0 +1,137 @@ +package creator.loki.command.impl.roles; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedTemplates; +import creator.loki.role.CustomRoleManager; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Role; + +import java.awt.Color; +import java.util.List; +import java.util.regex.Pattern; + +public class EditRoleCommand implements Command { + private static final Pattern HEX_PATTERN = Pattern.compile("^#?[0-9A-Fa-f]{6}$"); + private final CustomRoleManager roleManager; + + public EditRoleCommand(CustomRoleManager roleManager) { + this.roleManager = roleManager; + } + + @Override + public String getName() { + return "editrole"; + } + + @Override + public String getDescription() { + return "Изменяет цвет вашей кастомной роли"; + } + + @Override + public String getUsage() { + return "editrole <название> <новый #цвет>"; + } + + @Override + public String getCategory() { + return "Роли"; + } + + @Override + public void execute(CommandContext context) { + Guild guild = context.getGuild(); + if (guild == null) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Команда доступна только на сервере") + ).queue(); + return; + } + + if (!guild.getSelfMember().hasPermission(Permission.MANAGE_ROLES)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "У бота нет прав на управление ролями") + ).queue(); + return; + } + + if (context.getArgs().size() < 2) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Неверное использование", "Используй: `!editrole <название> <#цвет>`") + ).queue(); + return; + } + + // Парсинг: всё кроме последнего = название, последний = цвет + String colorArg = context.getArgs().get(context.getArgs().size() - 1); + String roleName = String.join(" ", context.getArgs().subList(0, context.getArgs().size() - 1)); + + // Валидация HEX цвета + if (!HEX_PATTERN.matcher(colorArg).matches()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Неверный цвет", "Укажите цвет в HEX формате (например: #FF5733)") + ).queue(); + return; + } + + Color color; + try { + String hex = colorArg.startsWith("#") ? colorArg : "#" + colorArg; + color = Color.decode(hex); + } catch (NumberFormatException e) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Неверный цвет", "Не удалось распознать цвет: " + colorArg) + ).queue(); + return; + } + + // Поиск роли + List roles = guild.getRolesByName(roleName, true); + if (roles.isEmpty()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Роль не найдена", "Роль с названием `" + roleName + "` не найдена") + ).queue(); + return; + } + + Role role = roles.get(0); + String userId = context.getAuthor().getId(); + String guildId = guild.getId(); + + // Проверка владения + if (!roleManager.isOwnedBy(guildId, userId, role.getId())) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Нет прав", "Вы можете изменять только свои кастомные роли") + ).queue(); + return; + } + + // Проверка позиции + if (!guild.getSelfMember().canInteract(role)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Невозможно изменить", "Роль находится выше роли бота в иерархии") + ).queue(); + return; + } + + // Изменение цвета + role.getManager().setColor(color).queue( + success -> { + String hex = String.format("#%06X", color.getRGB() & 0xFFFFFF); + context.getChannel().sendMessageEmbeds( + EmbedTemplates.success( + "Цвет изменён", + "Цвет роли " + role.getAsMention() + " изменён на " + hex + ) + ).queue(); + }, + error -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Не удалось изменить цвет: " + error.getMessage()) + ).queue(); + } + ); + } +} diff --git a/src/main/java/creator/loki/command/impl/roles/MyRolesCommand.java b/src/main/java/creator/loki/command/impl/roles/MyRolesCommand.java new file mode 100644 index 0000000..5001b17 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/roles/MyRolesCommand.java @@ -0,0 +1,122 @@ +package creator.loki.command.impl.roles; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.embed.EmbedTemplates; +import creator.loki.role.CustomRoleManager; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Role; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +public class MyRolesCommand implements Command { + private final CustomRoleManager roleManager; + + public MyRolesCommand(CustomRoleManager roleManager) { + this.roleManager = roleManager; + } + + @Override + public String getName() { + return "myroles"; + } + + @Override + public String getDescription() { + return "Показывает ваши кастомные роли"; + } + + @Override + public String getCategory() { + return "Роли"; + } + + @Override + public void execute(CommandContext context) { + Guild guild = context.getGuild(); + if (guild == null) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Команда доступна только на сервере") + ).queue(); + return; + } + + String userId = context.getAuthor().getId(); + String guildId = guild.getId(); + + List roleIds = roleManager.getUserRoleIds(guildId, userId); + + if (roleIds.isEmpty()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.info( + "Нет кастомных ролей", + "У вас пока нет кастомных ролей\n" + + "Создайте роль командой `!createrole <название> <#цвет>`" + ) + ).queue(); + return; + } + + // Получаем роли и проверяем их существование + List existingRoles = new ArrayList<>(); + List toRemove = new ArrayList<>(); + + for (String roleId : roleIds) { + Role role = guild.getRoleById(roleId); + if (role != null) { + existingRoles.add(role); + } else { + // Роль была удалена вручную - убираем из отслеживания + toRemove.add(roleId); + } + } + + // Очистка несуществующих ролей + for (String roleId : toRemove) { + roleManager.untrackRole(guildId, userId, roleId); + } + + if (existingRoles.isEmpty()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.info( + "Нет активных ролей", + "Ваши кастомные роли были удалены\n" + + "Создайте новую роль командой `!createrole <название> <#цвет>`" + ) + ).queue(); + return; + } + + // Формируем список ролей + StringBuilder rolesList = new StringBuilder(); + for (int i = 0; i < existingRoles.size(); i++) { + Role role = existingRoles.get(i); + rolesList.append((i + 1)).append(". ").append(role.getAsMention()); + + // Добавляем цвет роли + if (role.getColor() != null) { + String hex = String.format("#%06X", role.getColor().getRGB() & 0xFFFFFF); + rolesList.append(" (").append(hex).append(")"); + } + + rolesList.append("\n"); + } + + int current = existingRoles.size(); + int max = roleManager.getMaxRolesPerUser(); + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("🎭 Ваши кастомные роли") + .setDescription(rolesList.toString()) + .setColor(Color.BLUE) + .addField("Использовано", current + "/" + max, true) + .addField("Доступно", (max - current) + " " + (max - current == 1 ? "роль" : "ролей"), true) + .setFooter("Удалить роль: !deleterole <название>") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/roles/RoleCommandValidator.java b/src/main/java/creator/loki/command/impl/roles/RoleCommandValidator.java new file mode 100644 index 0000000..5ad876b --- /dev/null +++ b/src/main/java/creator/loki/command/impl/roles/RoleCommandValidator.java @@ -0,0 +1,67 @@ +package creator.loki.command.impl.roles; + +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedTemplates; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; + +/** + * Общие валидации для команд работы с ролями (DRY) + */ +public class RoleCommandValidator { + + public static boolean validateGuild(CommandContext context) { + if (context.getGuild() == null) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Команда доступна только на сервере") + ).queue(); + return false; + } + return true; + } + + public static boolean validateBotPermissions(Guild guild, CommandContext context) { + if (!guild.getSelfMember().hasPermission(Permission.MANAGE_ROLES)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "У бота нет прав на управление ролями") + ).queue(); + return false; + } + return true; + } + + public static boolean validateArgumentCount(CommandContext context, int minArgs, String usage) { + if (context.getArgs().size() < minArgs) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Неверное использование", usage) + ).queue(); + return false; + } + return true; + } + + public static boolean validateRoleName(String name, CommandContext context) { + if (name.isBlank()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Неверное название", "Название роли не может быть пустым") + ).queue(); + return false; + } + + if (name.length() > 100) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Слишком длинное название", "Максимальная длина названия — 100 символов") + ).queue(); + return false; + } + + if (name.contains("@everyone") || name.contains("@here")) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Запрещённое название", "Название не может содержать @everyone или @here") + ).queue(); + return false; + } + + return true; + } +} diff --git a/src/main/java/creator/loki/command/impl/setup/CounterSetupCommand.java b/src/main/java/creator/loki/command/impl/setup/CounterSetupCommand.java new file mode 100644 index 0000000..4d6e446 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/setup/CounterSetupCommand.java @@ -0,0 +1,159 @@ +package creator.loki.command.impl.setup; + +import creator.loki.database.ConfigRepository; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +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.SubcommandData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; + +/** + * Команда настройки счетчика участников + */ +public class CounterSetupCommand extends ListenerAdapter { + private static final Logger logger = LoggerFactory.getLogger(CounterSetupCommand.class); + private final ConfigRepository configRepo; + + public CounterSetupCommand(ConfigRepository configRepo) { + this.configRepo = configRepo; + } + + @Override + public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { + if (!"counter-setup".equals(event.getName())) { + return; + } + + // Проверка прав администратора + Member member = event.getMember(); + if (member == null || !member.hasPermission(Permission.ADMINISTRATOR)) { + event.reply("❌ У вас нет прав для использования этой команды!").setEphemeral(true).queue(); + return; + } + + String subcommand = event.getSubcommandName(); + if (subcommand == null) { + return; + } + + switch (subcommand) { + case "channel" -> handleChannel(event); + case "format" -> handleFormat(event); + case "info" -> handleInfo(event); + } + } + + /** + * Установить канал счетчика + */ + private void handleChannel(SlashCommandInteractionEvent event) { + VoiceChannel channel = event.getOption("channel").getAsChannel().asVoiceChannel(); + + configRepo.set("counter.channel_id", channel.getId()); + + // Сразу обновляем название канала + int memberCount = event.getGuild().getMemberCount(); + String format = configRepo.get("counter.format"); + if (format == null) { + format = "【👥】Участники: {count}"; + configRepo.set("counter.format", format); + } + + String newName = format.replace("{count}", String.valueOf(memberCount)); + channel.getManager().setName(newName).queue(); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("✅ Канал счетчика установлен") + .setDescription("Канал " + channel.getAsMention() + " теперь отображает количество участников") + .addField("Текущее количество", String.valueOf(memberCount), false) + .setColor(Color.GREEN); + + event.replyEmbeds(embed.build()).queue(); + logger.info("Установлен канал счетчика: {} ({})", channel.getName(), channel.getId()); + } + + /** + * Установить формат отображения + */ + private void handleFormat(SlashCommandInteractionEvent event) { + String format = event.getOption("format").getAsString(); + + if (!format.contains("{count}")) { + event.reply("❌ Формат должен содержать `{count}`!").setEphemeral(true).queue(); + return; + } + + configRepo.set("counter.format", format); + + // Обновляем канал если он установлен + String channelId = configRepo.get("counter.channel_id"); + if (channelId != null) { + VoiceChannel channel = event.getGuild().getVoiceChannelById(channelId); + if (channel != null) { + int memberCount = event.getGuild().getMemberCount(); + String newName = format.replace("{count}", String.valueOf(memberCount)); + channel.getManager().setName(newName).queue(); + } + } + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("✅ Формат обновлен") + .setDescription("Новый формат: `" + format + "`") + .addField("Пример", format.replace("{count}", "123"), false) + .setColor(Color.GREEN); + + event.replyEmbeds(embed.build()).queue(); + logger.info("Установлен формат счетчика: {}", format); + } + + /** + * Показать текущую конфигурацию + */ + private void handleInfo(SlashCommandInteractionEvent event) { + String channelId = configRepo.get("counter.channel_id"); + String format = configRepo.get("counter.format"); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("📊 Конфигурация счетчика участников") + .setColor(Color.BLUE); + + if (channelId != null) { + embed.addField("Канал", "<#" + channelId + ">", false); + } else { + embed.addField("Канал", "❌ Не установлен", false); + } + + if (format != null) { + embed.addField("Формат", "`" + format + "`", false); + embed.addField("Пример", format.replace("{count}", String.valueOf(event.getGuild().getMemberCount())), false); + } else { + embed.addField("Формат", "❌ Не установлен", false); + } + + event.replyEmbeds(embed.build()).setEphemeral(true).queue(); + } + + /** + * Зарегистрировать команду в Discord + */ + public static void register(net.dv8tion.jda.api.JDA jda) { + jda.updateCommands().addCommands( + Commands.slash("counter-setup", "Настройка счетчика участников") + .addSubcommands( + new SubcommandData("channel", "Установить канал для счетчика") + .addOption(OptionType.CHANNEL, "channel", "Голосовой канал", true), + new SubcommandData("format", "Установить формат отображения") + .addOption(OptionType.STRING, "format", "Формат (используйте {count})", true), + new SubcommandData("info", "Показать текущую конфигурацию") + ) + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/setup/LogSetupCommand.java b/src/main/java/creator/loki/command/impl/setup/LogSetupCommand.java new file mode 100644 index 0000000..a0b6b1c --- /dev/null +++ b/src/main/java/creator/loki/command/impl/setup/LogSetupCommand.java @@ -0,0 +1,122 @@ +package creator.loki.command.impl.setup; + +import creator.loki.database.ConfigRepository; +import net.dv8tion.jda.api.EmbedBuilder; +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.hooks.ListenerAdapter; +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.SubcommandData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; + +/** + * Команда настройки логирования + */ +public class LogSetupCommand extends ListenerAdapter { + private static final Logger logger = LoggerFactory.getLogger(LogSetupCommand.class); + private final ConfigRepository configRepo; + + public LogSetupCommand(ConfigRepository configRepo) { + this.configRepo = configRepo; + } + + @Override + public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { + if (!"log-setup".equals(event.getName())) { + return; + } + + Member member = event.getMember(); + if (member == null || !member.hasPermission(Permission.ADMINISTRATOR)) { + event.reply("❌ У вас нет прав для использования этой команды!").setEphemeral(true).queue(); + return; + } + + String subcommand = event.getSubcommandName(); + if (subcommand == null) { + return; + } + + switch (subcommand) { + case "moderation" -> handleModeration(event); + case "levelup" -> handleLevelUp(event); + case "info" -> handleInfo(event); + } + } + + private void handleModeration(SlashCommandInteractionEvent event) { + TextChannel channel = event.getOption("channel").getAsChannel().asTextChannel(); + + configRepo.set("moderation.log_channel_id", channel.getId()); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("✅ Канал логов модерации установлен") + .setDescription("Логи модерации будут отправляться в " + channel.getAsMention()) + .addField("Что логируется", + "• Удаление сообщений\n" + + "• Баны/разбаны\n" + + "• Кики\n" + + "• Муты/размуты\n" + + "• Отключение микрофона/звука", false) + .setColor(Color.GREEN); + + event.replyEmbeds(embed.build()).queue(); + logger.info("Установлен канал логов модерации: {} ({})", channel.getName(), channel.getId()); + } + + private void handleLevelUp(SlashCommandInteractionEvent event) { + TextChannel channel = event.getOption("channel").getAsChannel().asTextChannel(); + + configRepo.set("levelup.notification_channel_id", channel.getId()); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("✅ Канал уведомлений о level up установлен") + .setDescription("Уведомления о повышении уровня будут отправляться в " + channel.getAsMention()) + .setColor(Color.GREEN); + + event.replyEmbeds(embed.build()).queue(); + logger.info("Установлен канал уведомлений level up: {} ({})", channel.getName(), channel.getId()); + } + + private void handleInfo(SlashCommandInteractionEvent event) { + String moderationChannelId = configRepo.get("moderation.log_channel_id"); + String levelupChannelId = configRepo.get("levelup.notification_channel_id"); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("📋 Конфигурация логирования") + .setColor(Color.BLUE); + + if (moderationChannelId != null) { + embed.addField("Логи модерации", "<#" + moderationChannelId + ">", false); + } else { + embed.addField("Логи модерации", "❌ Не установлен", false); + } + + if (levelupChannelId != null) { + embed.addField("Уведомления level up", "<#" + levelupChannelId + ">", false); + } else { + embed.addField("Уведомления level up", "❌ Не установлен", false); + } + + event.replyEmbeds(embed.build()).setEphemeral(true).queue(); + } + + public static void register(net.dv8tion.jda.api.JDA jda) { + jda.updateCommands().addCommands( + Commands.slash("log-setup", "Настройка логирования") + .addSubcommands( + new SubcommandData("moderation", "Установить канал для логов модерации") + .addOption(OptionType.CHANNEL, "channel", "Текстовый канал", true), + new SubcommandData("levelup", "Установить канал для уведомлений о level up") + .addOption(OptionType.CHANNEL, "channel", "Текстовый канал", true), + new SubcommandData("info", "Показать текущую конфигурацию") + ) + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/setup/VoiceSetupCommand.java b/src/main/java/creator/loki/command/impl/setup/VoiceSetupCommand.java new file mode 100644 index 0000000..de9be9b --- /dev/null +++ b/src/main/java/creator/loki/command/impl/setup/VoiceSetupCommand.java @@ -0,0 +1,159 @@ +package creator.loki.command.impl.setup; + +import creator.loki.database.ConfigRepository; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.concrete.Category; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +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.SubcommandData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; + +/** + * Команда настройки системы личных голосовых каналов + */ +public class VoiceSetupCommand extends ListenerAdapter { + private static final Logger logger = LoggerFactory.getLogger(VoiceSetupCommand.class); + private final ConfigRepository configRepo; + + public VoiceSetupCommand(ConfigRepository configRepo) { + this.configRepo = configRepo; + } + + @Override + public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { + if (!"voice-setup".equals(event.getName())) { + return; + } + + // Проверка прав администратора + Member member = event.getMember(); + if (member == null || !member.hasPermission(Permission.ADMINISTRATOR)) { + event.reply("❌ У вас нет прав для использования этой команды!").setEphemeral(true).queue(); + return; + } + + String subcommand = event.getSubcommandName(); + if (subcommand == null) { + return; + } + + switch (subcommand) { + case "trigger" -> handleTrigger(event); + case "category" -> handleCategory(event); + case "text-channel" -> handleTextChannel(event); + case "info" -> handleInfo(event); + } + } + + /** + * Установить канал-триггер + */ + private void handleTrigger(SlashCommandInteractionEvent event) { + VoiceChannel channel = event.getOption("channel").getAsChannel().asVoiceChannel(); + + configRepo.set("voice.trigger_channel_id", channel.getId()); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("✅ Канал-триггер установлен") + .setDescription("Теперь при входе в " + channel.getAsMention() + " будет создаваться личный канал") + .setColor(Color.GREEN); + + event.replyEmbeds(embed.build()).queue(); + logger.info("Установлен канал-триггер: {} ({})", channel.getName(), channel.getId()); + } + + /** + * Установить категорию для личных каналов + */ + private void handleCategory(SlashCommandInteractionEvent event) { + Category category = event.getOption("category").getAsChannel().asCategory(); + + configRepo.set("voice.category_id", category.getId()); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("✅ Категория установлена") + .setDescription("Личные каналы будут создаваться в категории: **" + category.getName() + "**") + .setColor(Color.GREEN); + + event.replyEmbeds(embed.build()).queue(); + logger.info("Установлена категория: {} ({})", category.getName(), category.getId()); + } + + /** + * Установить текстовый канал для уведомлений + */ + private void handleTextChannel(SlashCommandInteractionEvent event) { + TextChannel channel = event.getOption("channel").getAsChannel().asTextChannel(); + + configRepo.set("voice.text_channel_id", channel.getId()); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("✅ Текстовый канал установлен") + .setDescription("Панели управления будут отправляться в " + channel.getAsMention()) + .setColor(Color.GREEN); + + event.replyEmbeds(embed.build()).queue(); + logger.info("Установлен текстовый канал: {} ({})", channel.getName(), channel.getId()); + } + + /** + * Показать текущую конфигурацию + */ + private void handleInfo(SlashCommandInteractionEvent event) { + String triggerId = configRepo.get("voice.trigger_channel_id"); + String categoryId = configRepo.get("voice.category_id"); + String textChannelId = configRepo.get("voice.text_channel_id"); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("🎤 Конфигурация системы личных каналов") + .setColor(Color.BLUE); + + if (triggerId != null) { + embed.addField("Канал-триггер", "<#" + triggerId + ">", false); + } else { + embed.addField("Канал-триггер", "❌ Не установлен", false); + } + + if (categoryId != null) { + Category category = event.getGuild().getCategoryById(categoryId); + embed.addField("Категория", category != null ? category.getName() : "❌ Не найдена", false); + } else { + embed.addField("Категория", "❌ Не установлена", false); + } + + if (textChannelId != null) { + embed.addField("Текстовый канал", "<#" + textChannelId + ">", false); + } else { + embed.addField("Текстовый канал", "❌ Не установлен", false); + } + + event.replyEmbeds(embed.build()).setEphemeral(true).queue(); + } + + /** + * Зарегистрировать команду в Discord + */ + public static void register(net.dv8tion.jda.api.JDA jda) { + jda.updateCommands().addCommands( + Commands.slash("voice-setup", "Настройка системы личных голосовых каналов") + .addSubcommands( + new SubcommandData("trigger", "Установить канал-триггер") + .addOption(OptionType.CHANNEL, "channel", "Голосовой канал", true), + new SubcommandData("category", "Установить категорию для личных каналов") + .addOption(OptionType.CHANNEL, "category", "Категория", true), + new SubcommandData("text-channel", "Установить текстовый канал для панелей управления") + .addOption(OptionType.CHANNEL, "channel", "Текстовый канал", true), + new SubcommandData("info", "Показать текущую конфигурацию") + ) + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/stats/LeaderboardButtonListener.java b/src/main/java/creator/loki/command/impl/stats/LeaderboardButtonListener.java new file mode 100644 index 0000000..af13d27 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/stats/LeaderboardButtonListener.java @@ -0,0 +1,51 @@ +package creator.loki.command.impl.stats; + +import creator.loki.level.LevelSystem; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; + +/** + * Обработчик кнопок таблицы лидеров (Single Responsibility) + */ +public class LeaderboardButtonListener extends ListenerAdapter { + private final LevelSystem levelSystem; + + public LeaderboardButtonListener(LevelSystem levelSystem) { + this.levelSystem = levelSystem; + } + + @Override + public void onButtonInteraction(ButtonInteractionEvent event) { + String buttonId = event.getComponentId(); + + if (!buttonId.startsWith("lb_")) { + return; + } + + event.deferEdit().queue(); + + switch (buttonId) { + case "lb_level" -> showLevelLeaderboard(event); + case "lb_messages" -> showMessageLeaderboard(event); + case "lb_voice" -> showVoiceLeaderboard(event); + } + } + + private void showLevelLeaderboard(ButtonInteractionEvent event) { + var top = levelSystem.getTopByLevel(10); + var embed = LeaderboardFormatter.formatLevelLeaderboard(top); + event.getHook().editOriginalEmbeds(embed).queue(); + } + + private void showMessageLeaderboard(ButtonInteractionEvent event) { + var top = levelSystem.getTopByMessages(10); + var embed = LeaderboardFormatter.formatMessageLeaderboard(top); + event.getHook().editOriginalEmbeds(embed).queue(); + } + + private void showVoiceLeaderboard(ButtonInteractionEvent event) { + var top = levelSystem.getTopByVoice(10); + var embed = LeaderboardFormatter.formatVoiceLeaderboard(top); + event.getHook().editOriginalEmbeds(embed).queue(); + } +} diff --git a/src/main/java/creator/loki/command/impl/stats/LeaderboardButtons.java b/src/main/java/creator/loki/command/impl/stats/LeaderboardButtons.java new file mode 100644 index 0000000..64e87b8 --- /dev/null +++ b/src/main/java/creator/loki/command/impl/stats/LeaderboardButtons.java @@ -0,0 +1,19 @@ +package creator.loki.command.impl.stats; + +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +import java.util.List; + +/** + * Кнопки для таблицы лидеров + */ +public class LeaderboardButtons { + + public static List