Initial commit: Discord bot with XP system, voice channels, moderation logs, and member counter

This commit is contained in:
loki5512344 2026-05-11 13:55:12 +02:00
commit 0f664e317e
132 changed files with 12415 additions and 0 deletions

33
.gitignore vendored Normal file
View file

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

235
FEATURES.md Normal file
View file

@ -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 <amount>` - очистить сообщения
- Автологирование всех действий
### 🎨 Кастомные роли
- `/createrole` - создать роль
- `/deleterole` - удалить роль
- `/editrole` - редактировать роль
- `/myroles` - мои роли
### 📈 Статистика
- `/stats [@user]` - статистика пользователя
- `/rep/@minusrep` - репутация
- `/reptop` - топ по репутации
- `/serverstats` - статистика сервера
### 🔧 Утилиты
- `/avatar [@user]` - аватар пользователя
- `/info [@user]` - информация о пользователе
- `/poll` - создать голосование
- `/ping` - пинг бота
- `/help` - справка
- `/tts <text>` - озвучить текст
---
## ⚙️ Команды настройки (только админы)
### Личные войс-каналы
```
/voice-setup trigger <channel> - канал-триггер
/voice-setup category <category> - категория для каналов
/voice-setup text-channel <channel> - текстовый канал для панелей
/voice-setup info - показать настройки
```
### Счетчик участников
```
/counter-setup channel <channel> - установить канал
/counter-setup format <format> - формат ({count})
/counter-setup info - показать настройки
```
### Логирование
```
/log-setup moderation <channel> - канал логов модерации
/log-setup levelup <channel> - канал уведомлений 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

46
README.md Normal file
View file

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

275
TODO.md Normal file
View file

@ -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 <channel>` - установить канал-триггер
- `/voice-setup category <category>` - установить категорию для личных каналов
- `/voice-setup text-channel <channel>` - установить текстовый канал для уведомлений
#### Counter Setup:
- `/counter-setup channel <voice-channel>` - установить канал для счетчика
- `/counter-setup format <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

75
build.gradle Normal file
View file

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

10
gradle.properties Normal file
View file

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

View file

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

1
settings.gradle Normal file
View file

@ -0,0 +1 @@
rootProject.name = 'discord-bot'

View file

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

View file

@ -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<Long, GuildMusicManager> musicManagers;
private final Map<Long, Long> lastActivityTime; // guildId -> timestamp
private final Map<Long, TtsQueue> ttsQueues; // guildId -> TtsQueue
private final ScheduledExecutorService cleanupScheduler;
public AudioManager() {
this.musicManagers = new ConcurrentHashMap<>();
this.lastActivityTime = new ConcurrentHashMap<>();
this.ttsQueues = new ConcurrentHashMap<>();
this.playerManager = new DefaultAudioPlayerManager();
this.playerManager.getConfiguration().setFrameBufferFactory(NonAllocatingAudioFrameBuffer::new);
AudioSourceManagers.registerRemoteSources(playerManager);
AudioSourceManagers.registerLocalSource(playerManager);
// Запускаем периодическую очистку неактивных менеджеров
this.cleanupScheduler = Executors.newSingleThreadScheduledExecutor();
this.cleanupScheduler.scheduleAtFixedRate(
this::cleanupInactiveManagers,
CLEANUP_INTERVAL_MINUTES,
CLEANUP_INTERVAL_MINUTES,
TimeUnit.MINUTES
);
logger.info("AudioManager инициализирован с автоочисткой каждые {} минут", CLEANUP_INTERVAL_MINUTES);
}
public synchronized GuildMusicManager getGuildAudioPlayer(Guild guild) {
long guildId = guild.getIdLong();
GuildMusicManager musicManager = musicManagers.get(guildId);
if (musicManager == null) {
musicManager = new GuildMusicManager(playerManager);
musicManagers.put(guildId, musicManager);
logger.debug("Создан новый GuildMusicManager для гильдии: {}", guild.getName());
}
// Обновляем время последней активности
lastActivityTime.put(guildId, System.currentTimeMillis());
guild.getAudioManager().setSendingHandler(musicManager.getSendHandler());
return musicManager;
}
/**
* Очищает неактивные GuildMusicManager
*/
private void cleanupInactiveManagers() {
long currentTime = System.currentTimeMillis();
long thresholdTime = currentTime - TimeUnit.MINUTES.toMillis(INACTIVE_THRESHOLD_MINUTES);
musicManagers.entrySet().removeIf(entry -> {
long guildId = entry.getKey();
Long lastActivity = lastActivityTime.get(guildId);
if (lastActivity == null || lastActivity < thresholdTime) {
GuildMusicManager manager = entry.getValue();
// Проверяем, что плеер не активен
if (manager.getPlayer().getPlayingTrack() == null) {
logger.info("Удаление неактивного GuildMusicManager для гильдии ID: {}", guildId);
manager.getPlayer().destroy();
lastActivityTime.remove(guildId);
return true;
}
}
return false;
});
}
/**
* Принудительно удаляет GuildMusicManager для гильдии
*/
public synchronized void removeGuildAudioPlayer(long guildId) {
GuildMusicManager manager = musicManagers.remove(guildId);
if (manager != null) {
manager.getPlayer().destroy();
lastActivityTime.remove(guildId);
logger.debug("GuildMusicManager удалён для гильдии ID: {}", guildId);
}
}
/**
* Получить или создать TtsQueue для гильдии
*/
public TtsQueue getTtsQueue(Guild guild) {
long guildId = guild.getIdLong();
return ttsQueues.computeIfAbsent(guildId, id -> {
logger.info("Создана новая TtsQueue для гильдии: {}", guild.getName());
return new TtsQueue();
});
}
/**
* Добавить TTS запрос в очередь
*/
public void enqueueTts(Guild guild, String text, net.dv8tion.jda.api.entities.channel.middleman.MessageChannel channel) {
GuildMusicManager musicManager = getGuildAudioPlayer(guild);
TtsQueue queue = getTtsQueue(guild);
TtsRequest request = new TtsRequest(this, musicManager, guild, text, channel);
queue.enqueue(request);
logger.info("TTS запрос добавлен для гильдии: {} (очередь: {})",
guild.getName(), queue.getQueueSize());
}
public AudioPlayerManager getPlayerManager() {
return playerManager;
}
/**
* Завершает работу AudioManager
*/
public void shutdown() {
logger.info("Завершение работы AudioManager...");
// Останавливаем все TTS очереди
ttsQueues.values().forEach(TtsQueue::shutdown);
ttsQueues.clear();
// Останавливаем scheduler
cleanupScheduler.shutdown();
try {
if (!cleanupScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupScheduler.shutdownNow();
}
} catch (InterruptedException e) {
cleanupScheduler.shutdownNow();
Thread.currentThread().interrupt();
}
// Уничтожаем все плееры
musicManagers.values().forEach(manager -> manager.getPlayer().destroy());
musicManagers.clear();
lastActivityTime.clear();
logger.info("AudioManager завершён");
}
}

View file

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

View file

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

View file

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

View file

@ -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<TtsRequest> queue = new LinkedBlockingQueue<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ExecutorService processor = Executors.newSingleThreadExecutor();
private ScheduledFuture<?> disconnectTask;
private volatile boolean isProcessing = false;
private volatile boolean isShutdown = false;
public TtsQueue() {
startProcessor();
}
/**
* Добавить TTS запрос в очередь
*/
public void enqueue(TtsRequest request) {
if (isShutdown) {
logger.warn("TtsQueue уже завершена, запрос отклонён");
return;
}
queue.offer(request);
cancelDisconnectTimer();
logger.info("TTS запрос добавлен в очередь. Размер очереди: {}", queue.size());
}
/**
* Запустить обработчик очереди
*/
private void startProcessor() {
processor.submit(() -> {
while (!isShutdown) {
try {
TtsRequest request = queue.poll(1, TimeUnit.SECONDS);
if (request != null) {
processRequest(request);
} else if (!isProcessing && queue.isEmpty()) {
scheduleDisconnect(request != null ? request : getLastRequest());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
logger.error("Ошибка обработки TTS запроса", e);
}
}
});
}
/**
* Обработать один TTS запрос
*/
private void processRequest(TtsRequest request) {
isProcessing = true;
cancelDisconnectTimer();
try {
logger.info("Обработка TTS: {}", request.getText());
request.process();
// Ждём завершения воспроизведения
waitForPlaybackEnd(request);
} catch (Exception e) {
logger.error("Ошибка воспроизведения TTS", e);
} finally {
isProcessing = false;
}
}
/**
* Ждать завершения воспроизведения
*/
private void waitForPlaybackEnd(TtsRequest request) {
try {
// Ждём максимум 30 секунд
for (int i = 0; i < 30; i++) {
if (!request.isPlaying()) {
break;
}
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* Запланировать отключение через 30 секунд
*/
private void scheduleDisconnect(TtsRequest lastRequest) {
if (disconnectTask != null && !disconnectTask.isDone()) {
return; // Уже запланировано
}
disconnectTask = scheduler.schedule(() -> {
if (queue.isEmpty() && !isProcessing && lastRequest != null) {
logger.info("30 секунд бездействия, отключаемся от голосового канала");
lastRequest.disconnect();
}
}, DISCONNECT_DELAY_SECONDS, TimeUnit.SECONDS);
logger.info("Запланировано отключение через {} секунд", DISCONNECT_DELAY_SECONDS);
}
/**
* Отменить таймер отключения
*/
private void cancelDisconnectTimer() {
if (disconnectTask != null && !disconnectTask.isDone()) {
disconnectTask.cancel(false);
logger.debug("Таймер отключения отменён");
}
}
/**
* Получить последний запрос (для отключения)
*/
private TtsRequest getLastRequest() {
return queue.peek();
}
/**
* Завершить работу очереди
*/
public void shutdown() {
isShutdown = true;
cancelDisconnectTimer();
scheduler.shutdown();
processor.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
if (!processor.awaitTermination(5, TimeUnit.SECONDS)) {
processor.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
processor.shutdownNow();
Thread.currentThread().interrupt();
}
logger.info("TtsQueue завершена");
}
/**
* Получить размер очереди
*/
public int getQueueSize() {
return queue.size();
}
/**
* Проверить, обрабатывается ли запрос
*/
public boolean isProcessing() {
return isProcessing;
}
}

View file

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

View file

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

View file

@ -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<String> args;
private final MessageChannel channel;
private final Member author;
private final Guild guild;
public CommandContext(Message message, List<String> 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<String> getArgs() {
return args;
}
public MessageChannel getChannel() {
return channel;
}
public Member getAuthor() {
return author;
}
public Guild getGuild() {
return guild;
}
}

View file

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

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не целуют мальчиков!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "boykisser", 5);
// Детерминированный процент
int boykisserLevel = getDeterministicValue(target.getId(), 777, 0, 100);
// Прогресс-бар с поцелуями
StringBuilder bar = new StringBuilder();
int filled = boykisserLevel / 10;
for (int i = 0; i < filled; i++) {
bar.append("💋");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
// Комментарий
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;
}
}

View file

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

View file

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

View file

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

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "fat", 5);
// Детерминированный процент от 0 до 100
int fatLevel = getDeterministicValue(target.getId(), 999, 0, 100);
// Прогресс-бар
StringBuilder bar = new StringBuilder();
int filled = fatLevel / 10;
for (int i = 0; i < filled; i++) {
bar.append("🟩");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
// Комментарий
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;
}
}

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не имеют ориентации!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "gay", 5);
// Детерминированный процент
int gayLevel = getDeterministicValue(target.getId(), 456, 0, 100);
// Прогресс-бар с радугой
String[] rainbow = {"🟥", "🟧", "🟨", "🟩", "🟦", "🟪"};
StringBuilder bar = new StringBuilder();
int filled = Math.min(gayLevel / 17 + 1, 6);
for (int i = 0; i < filled; i++) {
bar.append(rainbow[i]);
}
for (int i = filled; i < 6; i++) {
bar.append("⬜");
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.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;
}
}

View file

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

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "iq", 5);
int iq;
if (target.getUser().isBot()) {
iq = 9000; // Боты умные!
} else if (target.getId().equals("1414975197269987471")) {
iq = 1000; // Специальное значение
} else {
// Детерминированный IQ от 0 до 140
iq = getDeterministicValue(target.getId(), 123, 0, 140);
}
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;
}
}

View file

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

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "mustache", 5);
int mustacheLevel;
// Специальные значения
if (target.getUser().isBot() || target.getId().equals("1414975197269987471")) {
mustacheLevel = 0; // Боты и специальный ID = 0%
} else if (target.getId().equals("678283217094180915")) {
mustacheLevel = 1000; // Специальный ID = 1000%
} else {
// Детерминированный процент от 0 до 100
mustacheLevel = getDeterministicValue(target.getId(), 888, 0, 100);
}
// Прогресс-бар с усами
StringBuilder bar = new StringBuilder();
int filled = Math.min(mustacheLevel / 10, 10);
for (int i = 0; i < filled; i++) {
bar.append("🧔");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
// Комментарий
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;
}
}

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ У ботов нет pp!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "pp", 5);
// Специальное значение для конкретного пользователя
int size;
if (target.getId().equals("1414975197269987471")) {
size = 100;
} else {
// Детерминированный размер от 0 до 30 см
size = getDeterministicValue(target.getId(), 6969, 0, 30);
}
// Визуализация
StringBuilder ppVisual = new StringBuilder("8");
for (int i = 0; i < size; i++) {
ppVisual.append("=");
}
ppVisual.append("D");
// Определяем уровень
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;
}
}

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "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<Integer, String> 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;
}
}

View file

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

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
cooldownManager.applyCooldown(context.getAuthor().getId(), "cuddle", 5);
String gifUrl = NekosLifeApi.fetchImage("cuddle");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setDescription("🥰 " + context.getAuthor().getAsMention() + " обнимается с " + target.getAsMention() + "! 💕")
.setImage(gifUrl)
.setColor(new Color(255, 0, 255))
.setFooter("Powered by nekos.life")
.build()
).queue();
}
}

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
cooldownManager.applyCooldown(context.getAuthor().getId(), "feed", 5);
String gifUrl = NekosLifeApi.fetchImage("feed");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setDescription("🍽️ " + context.getAuthor().getAsMention() + " кормит " + target.getAsMention() + "! 😋")
.setImage(gifUrl)
.setColor(new Color(255, 200, 100))
.setFooter("Powered by nekos.life")
.build()
).queue();
}
}

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
// Получаем GIF из nekos.life API
String gifUrl = NekosLifeApi.fetchImage("hug");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setDescription("🤗 " + context.getAuthor().getAsMention() + " обнял " + target.getAsMention() + " 💕")
.setImage(gifUrl)
.setColor(new Color(255, 182, 193))
.setFooter("Powered by nekos.life")
.build()
).queue();
}
}

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи кого поцеловать! Использование: `!kiss @user`").queue();
return;
}
Member target = mentioned.get(0);
if (target.getId().equals(context.getAuthor().getId())) {
context.getChannel().sendMessage("❌ Нельзя целовать самого себя!").queue();
return;
}
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не умеют целоваться!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "kiss", 5);
// Получаем GIF из nekos.life API
String gifUrl = NekosLifeApi.fetchImage("kiss");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setDescription("💋 " + context.getAuthor().getAsMention() + " целует " + target.getAsMention() + " ❤️")
.setImage(gifUrl)
.setColor(new Color(255, 105, 180))
.setFooter("Powered by nekos.life")
.build()
).queue();
}
}

View file

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

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
cooldownManager.applyCooldown(context.getAuthor().getId(), "poke", 5);
String gifUrl = NekosLifeApi.fetchImage("poke");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setDescription("👉 " + context.getAuthor().getAsMention() + " тыкает в " + target.getAsMention() + "! 😏")
.setImage(gifUrl)
.setColor(new Color(0, 128, 128))
.setFooter("Powered by nekos.life")
.build()
).queue();
}
}

View file

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

View file

@ -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<Member> 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<Map<String, Integer>>(){}.getType();
Map<String, Integer> 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); // Серый
}
}

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
cooldownManager.applyCooldown(context.getAuthor().getId(), "slap", 5);
String gifUrl = NekosLifeApi.fetchImage("slap");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setDescription("👋 " + context.getAuthor().getAsMention() + " шлепает " + target.getAsMention() + "! 😠")
.setImage(gifUrl)
.setColor(new Color(255, 165, 0))
.setFooter("Powered by nekos.life")
.build()
).queue();
}
}

View file

@ -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<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("❌ Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
cooldownManager.applyCooldown(context.getAuthor().getId(), "tickle", 5);
String gifUrl = NekosLifeApi.fetchImage("tickle");
if (gifUrl == null) {
context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue();
return;
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setDescription("🤗 " + context.getAuthor().getAsMention() + " щекочет " + target.getAsMention() + "!")
.setImage(gifUrl)
.setColor(new Color(0, 255, 0))
.setFooter("Powered by nekos.life")
.build()
).queue();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<String> roleIds = roleManager.getUserRoleIds(guildId, userId);
if (roleIds.isEmpty()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.info(
"Нет кастомных ролей",
"У вас пока нет кастомных ролей\n" +
"Создайте роль командой `!createrole <название> <#цвет>`"
)
).queue();
return;
}
// Получаем роли и проверяем их существование
List<Role> existingRoles = new ArrayList<>();
List<String> 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();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Button> getButtons() {
return List.of(
Button.primary("lb_level", "📊 Уровни"),
Button.success("lb_messages", "💬 Сообщения"),
Button.secondary("lb_voice", "🎤 Войс")
);
}
}

View file

@ -0,0 +1,89 @@
package creator.loki.command.impl.stats;
import creator.loki.embed.EmbedFactory;
import creator.loki.level.UserStats;
import net.dv8tion.jda.api.entities.MessageEmbed;
import java.awt.Color;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* Форматирование таблиц лидеров (DRY принцип)
*/
public class LeaderboardFormatter {
public static MessageEmbed formatLevelLeaderboard(List<Map.Entry<String, UserStats>> top) {
return formatLeaderboard(
top,
"🏆 Топ-10 по уровням",
new Color(255, 215, 0),
entry -> {
UserStats stats = entry.getValue();
return "Уровень **" + stats.getLevel() + "** (" + stats.getTotalXP() + " XP)";
}
);
}
public static MessageEmbed formatMessageLeaderboard(List<Map.Entry<String, UserStats>> top) {
return formatLeaderboard(
top,
"💬 Топ-10 по сообщениям",
new Color(52, 152, 219),
entry -> "**" + entry.getValue().getMessageCount() + "** сообщений"
);
}
public static MessageEmbed formatVoiceLeaderboard(List<Map.Entry<String, UserStats>> top) {
return formatLeaderboard(
top,
"🎤 Топ-10 по времени в войсе",
new Color(155, 89, 182),
entry -> {
long minutes = entry.getValue().getVoiceMinutes();
long hours = minutes / 60;
long mins = minutes % 60;
return "**" + hours + "ч " + mins + "м** в войсе";
}
);
}
private static MessageEmbed formatLeaderboard(
List<Map.Entry<String, UserStats>> top,
String title,
Color color,
Function<Map.Entry<String, UserStats>, String> statFormatter
) {
StringBuilder description = new StringBuilder();
int position = 1;
for (var entry : top) {
String emoji = getPositionEmoji(position);
String userId = entry.getKey();
String stats = statFormatter.apply(entry);
description.append(emoji)
.append(" **#").append(position).append("** <@").append(userId).append(">")
.append(" - ").append(stats).append("\n");
position++;
}
return EmbedFactory.create()
.setTitle(title)
.setDescription(description.toString())
.setColor(color)
.setFooter("Выбери категорию ниже")
.build();
}
private static String getPositionEmoji(int position) {
return switch (position) {
case 1 -> "🥇";
case 2 -> "🥈";
case 3 -> "🥉";
default -> "⭐";
};
}
}

View file

@ -0,0 +1,100 @@
package creator.loki.command.impl.stats;
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 creator.loki.util.CooldownManager;
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 MinusRepCommand implements Command {
private final JsonDataStore dataStore;
private final CooldownManager cooldownManager;
public MinusRepCommand(JsonDataStore dataStore, CooldownManager cooldownManager) {
this.dataStore = dataStore;
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "-rep";
}
@Override
public String getDescription() {
return "Забрать -1 репутацию у пользователя";
}
@Override
public String getUsage() {
return "-rep <@user>";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
// Нельзя забирать у себя
if (target.getId().equals(context.getAuthor().getId())) {
context.getChannel().sendMessage("Нельзя забирать репутацию у самого себя!").queue();
return;
}
// Проверка cooldown (24 часа = 86400 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "-rep")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "-rep");
long hours = remaining / 3600;
long minutes = (remaining % 3600) / 60;
context.getChannel().sendMessage(
"Ты сможешь забрать репутацию через " + hours + "ч " + minutes + "м"
).queue();
return;
}
// Получаем репутацию
Type type = new TypeToken<Map<String, Integer>>(){}.getType();
Map<String, Integer> reputation = dataStore.getData("reputation", type);
if (reputation == null) {
reputation = new HashMap<>();
}
// Забираем -1
int currentRep = reputation.getOrDefault(target.getId(), 0);
reputation.put(target.getId(), currentRep - 1);
dataStore.setData("reputation", reputation);
// Применяем cooldown 24 часа
cooldownManager.applyCooldown(context.getAuthor().getId(), "-rep", 86400);
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("⭐❌ Репутация")
.setDescription(
context.getAuthor().getAsMention() + " забрал репутацию у " + target.getAsMention() + " **(-1)**\n\n" +
"Текущая репутация: **" + (currentRep - 1) + "**"
)
.setColor(new Color(231, 76, 60))
.build()
).queue();
}
}

View file

@ -0,0 +1,58 @@
package creator.loki.command.impl.stats;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.level.LevelSystem;
import java.util.concurrent.TimeUnit;
/**
* Команда таблицы лидеров (рефакторинг: 241 → 50 строк)
*/
public class NewLeaderboardCommand implements Command {
private final LevelSystem levelSystem;
public NewLeaderboardCommand(LevelSystem levelSystem) {
this.levelSystem = levelSystem;
}
@Override
public String getName() {
return "leaderboard";
}
@Override
public String getDescription() {
return "Таблица лидеров (уровни, сообщения, войс)";
}
@Override
public String getUsage() {
return "leaderboard";
}
@Override
public String getCategory() {
return "Статистика";
}
@Override
public void execute(CommandContext context) {
var top = levelSystem.getTopByLevel(10);
if (top.isEmpty()) {
context.getChannel().sendMessage("❌ Пока нет статистики!").queue();
return;
}
var embed = LeaderboardFormatter.formatLevelLeaderboard(top);
var buttons = LeaderboardButtons.getButtons();
context.getChannel().sendMessageEmbeds(embed)
.setActionRow(buttons)
.queue(message -> {
// Удаляем кнопки через 5 минут
message.editMessageComponents().queueAfter(5, TimeUnit.MINUTES);
});
}
}

View file

@ -0,0 +1,88 @@
package creator.loki.command.impl.stats;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.level.LevelSystem;
import creator.loki.level.UserStats;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
/**
* Команда для просмотра уровня и статистики
*/
public class RankCommand implements Command {
private final LevelSystem levelSystem;
public RankCommand(LevelSystem levelSystem) {
this.levelSystem = levelSystem;
}
@Override
public String getName() {
return "rank";
}
@Override
public String getDescription() {
return "Посмотреть свой уровень и статистику";
}
@Override
public String getUsage() {
return "rank [@user]";
}
@Override
public String getCategory() {
return "Статистика";
}
@Override
public void execute(CommandContext context) {
Member target;
if (!context.getMessage().getMentions().getMembers().isEmpty()) {
target = context.getMessage().getMentions().getMembers().get(0);
} else {
target = context.getAuthor();
}
if (target == null) {
context.getChannel().sendMessage("❌ Пользователь не найден!").queue();
return;
}
String userId = target.getId();
UserStats stats = levelSystem.getUserStats(userId);
LevelSystem.LevelProgress progress = levelSystem.getProgress(userId);
// Прогресс-бар
int barLength = 20;
int filled = (int) (progress.getPercentage() / 100.0 * barLength);
StringBuilder progressBar = new StringBuilder();
for (int i = 0; i < barLength; i++) {
progressBar.append(i < filled ? "█" : "░");
}
long hours = stats.getVoiceMinutes() / 60;
long minutes = stats.getVoiceMinutes() % 60;
var embed = EmbedFactory.create()
.setTitle("📊 Статистика " + target.getEffectiveName())
.setThumbnail(target.getEffectiveAvatarUrl())
.addField("🎯 Уровень", "**" + stats.getLevel() + "**", true)
.addField("⭐ Всего XP", "**" + stats.getTotalXP() + "**", true)
.addField("📈 Прогресс",
progressBar.toString() + " **" + String.format("%.1f", progress.getPercentage()) + "%**\n" +
progress.getCurrentLevelXP() + " / " + progress.getXpForNextLevel() + " XP до следующего уровня",
false)
.addField("💬 Сообщений", "**" + stats.getMessageCount() + "**", true)
.addField("🎤 Время в войсе", "**" + hours + "ч " + minutes + "м**", true)
.setColor(new Color(46, 204, 113))
.build();
context.getChannel().sendMessageEmbeds(embed).queue();
}
}

View file

@ -0,0 +1,119 @@
package creator.loki.command.impl.stats;
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 creator.loki.util.CooldownManager;
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 RepCommand implements Command {
private final JsonDataStore dataStore;
private final CooldownManager cooldownManager;
public RepCommand(JsonDataStore dataStore, CooldownManager cooldownManager) {
this.dataStore = dataStore;
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "rep";
}
@Override
public String getDescription() {
return "Дать +1 репутацию пользователю";
}
@Override
public String getUsage() {
return "rep <@user>";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
// Если нет упоминаний - показать свою репутацию
if (mentioned.isEmpty()) {
showReputation(context, context.getAuthor());
return;
}
Member target = mentioned.get(0);
// Нельзя давать себе
if (target.getId().equals(context.getAuthor().getId())) {
context.getChannel().sendMessage("Нельзя давать репутацию самому себе!").queue();
return;
}
// Проверка cooldown (24 часа = 86400 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "rep")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "rep");
long hours = remaining / 3600;
long minutes = (remaining % 3600) / 60;
context.getChannel().sendMessage(
"Ты сможешь дать репутацию через " + hours + "ч " + minutes + "м"
).queue();
return;
}
// Получаем репутацию
Type type = new TypeToken<Map<String, Integer>>(){}.getType();
Map<String, Integer> reputation = dataStore.getData("reputation", type);
if (reputation == null) {
reputation = new HashMap<>();
}
// Добавляем +1
int currentRep = reputation.getOrDefault(target.getId(), 0);
reputation.put(target.getId(), currentRep + 1);
dataStore.setData("reputation", reputation);
// Применяем cooldown 24 часа
cooldownManager.applyCooldown(context.getAuthor().getId(), "rep", 86400);
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("⭐ Репутация")
.setDescription(
context.getAuthor().getAsMention() + " дал репутацию " + target.getAsMention() + " **(+1)**\n\n" +
"Текущая репутация: **" + (currentRep + 1) + "**"
)
.setColor(new Color(241, 196, 15))
.build()
).queue();
}
private void showReputation(CommandContext context, Member member) {
Type type = new TypeToken<Map<String, Integer>>(){}.getType();
Map<String, Integer> reputation = dataStore.getData("reputation", type);
int rep = 0;
if (reputation != null) {
rep = reputation.getOrDefault(member.getId(), 0);
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("⭐ Репутация")
.setDescription(member.getAsMention() + " имеет **" + rep + "** репутации")
.setColor(new Color(241, 196, 15))
.build()
).queue();
}
}

View file

@ -0,0 +1,114 @@
package creator.loki.command.impl.stats;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.data.JsonDataStore;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Топ пользователей по репутации
*/
public class RepTopCommand implements Command {
private final JsonDataStore dataStore;
public RepTopCommand(JsonDataStore dataStore) {
this.dataStore = dataStore;
}
@Override
public String getName() {
return "reptop";
}
@Override
public String getDescription() {
return "Топ-10 пользователей по репутации";
}
@Override
public String getUsage() {
return "reptop";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
Map<String, Integer> reputation = dataStore.getReputation();
if (reputation.isEmpty()) {
context.getChannel().sendMessage("❌ Пока никто не имеет репутации!").queue();
return;
}
// Сортируем по репутации и берём топ-10
var topUsers = reputation.entrySet().stream()
.sorted((e1, e2) -> Integer.compare(e2.getValue(), e1.getValue()))
.limit(10)
.collect(Collectors.toList());
StringBuilder description = new StringBuilder();
int position = 1;
for (var entry : topUsers) {
String userId = entry.getKey();
int rep = entry.getValue();
// Получаем пользователя
context.getGuild().retrieveMemberById(userId).queue(
member -> {
// Эмодзи для топ-3
String emoji = switch (position) {
case 1 -> "🥇";
case 2 -> "🥈";
case 3 -> "🥉";
default -> "⭐";
};
description.append(emoji)
.append(" **#")
.append(position)
.append("** ")
.append(member.getAsMention())
.append(" - **")
.append(rep)
.append("** репутации\n");
},
error -> {
// Если пользователь не найден
description.append("⭐ **#")
.append(position)
.append("** <@")
.append(userId)
.append("> - **")
.append(rep)
.append("** репутации\n");
}
);
}
// Отправляем embed после небольшой задержки, чтобы все пользователи загрузились
new Thread(() -> {
try {
Thread.sleep(1000);
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("🏆 Топ-10 по репутации")
.setDescription(description.toString())
.setColor(new Color(255, 215, 0))
.setFooter("Используй !rep <@user> чтобы дать репутацию")
.build()
).queue();
} catch (InterruptedException ignored) {
}
}).start();
}
}

View file

@ -0,0 +1,75 @@
package creator.loki.command.impl.stats;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
public class StatsCommand implements Command {
@Override
public String getName() {
return "stats";
}
@Override
public String getDescription() {
return "Статистика сервера";
}
@Override
public String getUsage() {
return "stats";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
if (context.getGuild() == null) {
context.getChannel().sendMessage("Команда доступна только на сервере").queue();
return;
}
Guild guild = context.getGuild();
// Подсчёт статистики
int totalMembers = guild.getMemberCount();
long onlineMembers = guild.getMembers().stream()
.filter(m -> m.getOnlineStatus() == OnlineStatus.ONLINE)
.count();
long inVoice = guild.getMembers().stream()
.filter(m -> m.getVoiceState() != null && m.getVoiceState().inAudioChannel())
.count();
int textChannels = guild.getTextChannels().size();
int voiceChannels = guild.getVoiceChannels().size();
int roles = guild.getRoles().size();
int bots = (int) guild.getMembers().stream()
.filter(m -> m.getUser().isBot())
.count();
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("📊 Статистика сервера")
.setThumbnail(guild.getIconUrl())
.addField("👥 Участников", String.valueOf(totalMembers), true)
.addField("🟢 Онлайн", String.valueOf(onlineMembers), true)
.addField("🔊 В войсе", String.valueOf(inVoice), true)
.addField("💬 Текстовых каналов", String.valueOf(textChannels), true)
.addField("🎤 Голосовых каналов", String.valueOf(voiceChannels), true)
.addField("🎭 Ролей", String.valueOf(roles), true)
.addField("🤖 Ботов", String.valueOf(bots), true)
.addField("👤 Людей", String.valueOf(totalMembers - bots), true)
.setColor(new Color(52, 152, 219))
.setFooter("Сервер: " + guild.getName())
.build()
).queue();
}
}

View file

@ -0,0 +1,58 @@
package creator.loki.command.impl.utility;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import java.awt.Color;
import java.util.List;
public class AvatarCommand implements Command {
@Override
public String getName() {
return "avatar";
}
@Override
public String getDescription() {
return "Показывает аватар пользователя";
}
@Override
public String getUsage() {
return "avatar [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
User targetUser;
// Если упомянут пользователь - показываем его аватар
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (!mentioned.isEmpty()) {
targetUser = mentioned.get(0).getUser();
} else {
// Иначе показываем аватар автора команды
targetUser = context.getAuthor().getUser();
}
String avatarUrl = targetUser.getEffectiveAvatarUrl() + "?size=512";
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("🖼️ Аватар " + targetUser.getName())
.setImage(avatarUrl)
.setColor(Color.BLUE)
.setFooter("ID: " + targetUser.getId())
.build()
).queue();
}
}

View file

@ -0,0 +1,113 @@
package creator.loki.command.impl.utility;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.core.CommandRegistry;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class HelpCommand implements Command {
private final CommandRegistry registry;
private final BotConfig config;
private static final Map<String, String> CATEGORY_EMOJIS = new HashMap<>();
private static final Map<String, Color> CATEGORY_COLORS = new HashMap<>();
static {
CATEGORY_EMOJIS.put("Общие", "📋");
CATEGORY_EMOJIS.put("Роли", "🎭");
CATEGORY_EMOJIS.put("Развлечения", "🎮");
CATEGORY_EMOJIS.put("Модерация", "🛡️");
CATEGORY_COLORS.put("Общие", new Color(52, 152, 219));
CATEGORY_COLORS.put("Роли", new Color(155, 89, 182));
CATEGORY_COLORS.put("Развлечения", new Color(241, 196, 15));
CATEGORY_COLORS.put("Модерация", new Color(231, 76, 60));
}
public HelpCommand(CommandRegistry registry, BotConfig config) {
this.registry = registry;
this.config = config;
}
@Override
public String getName() {
return "help";
}
@Override
public String getDescription() {
return "Показывает список всех команд";
}
@Override
public void execute(CommandContext context) {
var commands = registry.getAllCommands();
// Группируем команды по категориям
Map<String, List<Command>> commandsByCategory = commands.stream()
.collect(Collectors.groupingBy(Command::getCategory));
// Создаём красивый embed
EmbedFactory embed = EmbedFactory.create()
.setTitle("📚 Справка по командам")
.setDescription(
"**Префикс:** `" + config.getPrefix() + "`\n" +
"**Всего команд:** " + commands.size() + "\n\n" +
"Используй `" + config.getPrefix() + "<команда>` для выполнения"
)
.setColor(new Color(88, 101, 242))
.setThumbnail(context.getGuild() != null ?
context.getGuild().getIconUrl() : null);
// Добавляем команды по категориям
commandsByCategory.forEach((category, cmds) -> {
String emoji = CATEGORY_EMOJIS.getOrDefault(category, "📌");
StringBuilder commandList = new StringBuilder();
int count = 0;
int part = 1;
for (Command cmd : cmds) {
String cmdText = "`" + config.getPrefix() + cmd.getUsage() + "`\n" +
cmd.getDescription() + "\n\n";
// Если добавление команды превысит лимит, создаём новое поле
if (commandList.length() + cmdText.length() > 1000) {
embed.addField(
emoji + " " + category + (part > 1 ? " (" + part + ")" : ""),
commandList.toString().trim(),
false
);
commandList = new StringBuilder();
part++;
}
commandList.append(cmdText);
count++;
}
// Добавляем оставшиеся команды
if (commandList.length() > 0) {
embed.addField(
emoji + " " + category + (part > 1 ? " (" + part + ")" : ""),
commandList.toString().trim(),
false
);
}
});
embed.setFooter(
"Запросил: " + context.getAuthor().getUser().getName(),
context.getAuthor().getUser().getAvatarUrl()
);
context.getChannel().sendMessageEmbeds(embed.build()).queue();
}
}

View file

@ -0,0 +1,116 @@
package creator.loki.command.impl.utility;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.User;
import java.awt.Color;
import java.time.format.DateTimeFormatter;
import java.util.stream.Collectors;
public class InfoCommand implements Command {
@Override
public String getName() {
return "info";
}
@Override
public String getDescription() {
return "Информация о сервере или пользователе";
}
@Override
public String getUsage() {
return "info [server|user]";
}
@Override
public void execute(CommandContext context) {
String type = context.getArgs().isEmpty() ? "server" : context.getArgs().get(0).toLowerCase();
switch (type) {
case "server":
showServerInfo(context);
break;
case "user":
showUserInfo(context);
break;
default:
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"Неверный аргумент",
"Используй: `!info [server|user]`"
)
).queue();
}
}
private void showServerInfo(CommandContext context) {
Guild guild = context.getGuild();
if (guild == null) return;
String iconUrl = guild.getIconUrl();
if (iconUrl == null) iconUrl = "";
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("🏰 Информация о сервере")
.setColor(Color.BLUE)
.setThumbnail(iconUrl)
.addField("Название", guild.getName(), true)
.addField("ID", guild.getId(), true)
.addField("Владелец", guild.getOwner() != null ? guild.getOwner().getAsMention() : "Неизвестно", true)
.addField("Участников", String.valueOf(guild.getMemberCount()), true)
.addField("Каналов", String.valueOf(guild.getChannels().size()), true)
.addField("Ролей", String.valueOf(guild.getRoles().size()), true)
.addField(
"Создан",
guild.getTimeCreated().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")),
false
)
.build()
).queue();
}
private void showUserInfo(CommandContext context) {
Member member = context.getAuthor();
if (member == null) return;
User user = member.getUser();
String roles = member.getRoles().stream()
.map(Role::getAsMention)
.collect(Collectors.joining(", "));
if (roles.isEmpty()) {
roles = "Нет ролей";
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("👤 Информация о пользователе")
.setColor(Color.CYAN)
.setThumbnail(user.getEffectiveAvatarUrl())
.addField("Имя", user.getName(), true)
.addField("ID", user.getId(), true)
.addField("Никнейм", member.getNickname() != null ? member.getNickname() : "Не установлен", true)
.addField("Бот?", user.isBot() ? "Да" : "Нет", true)
.addField(
"Присоединился",
member.getTimeJoined().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")),
false
)
.addField(
"Роли (" + member.getRoles().size() + ")",
roles,
false
)
.build()
).queue();
}
}

View file

@ -0,0 +1,63 @@
package creator.loki.command.impl.utility;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.metrics.MetricsCollector;
import net.dv8tion.jda.api.Permission;
import java.awt.Color;
public class MetricsCommand implements Command {
private final MetricsCollector metrics;
public MetricsCommand(MetricsCollector metrics) {
this.metrics = metrics;
}
@Override
public void execute(CommandContext ctx) {
// Только администраторы могут видеть метрики
if (ctx.getAuthor() == null || !ctx.getAuthor().hasPermission(Permission.ADMINISTRATOR)) {
ctx.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("❌ Недостаточно прав")
.setDescription("Эта команда доступна только администраторам.")
.setColor(Color.RED)
.build()
).queue();
return;
}
String stats = metrics.getFullStats();
ctx.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("📊 Метрики бота")
.setDescription("```\n" + stats + "\n```")
.setColor(new Color(88, 101, 242))
.setFooter("Используй !metrics reset для сброса")
.build()
).queue();
}
@Override
public String getName() {
return "metrics";
}
@Override
public String getDescription() {
return "Показывает метрики и статистику бота (только для администраторов)";
}
@Override
public String getUsage() {
return "!metrics";
}
@Override
public String getCategory() {
return "Система";
}
}

View file

@ -0,0 +1,39 @@
package creator.loki.command.impl.utility;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
public class PingCommand implements Command {
@Override
public String getName() {
return "ping";
}
@Override
public String getDescription() {
return "Проверка задержки бота";
}
@Override
public void execute(CommandContext context) {
long startTime = System.currentTimeMillis();
context.getChannel().sendMessage("Измеряю задержку...").queue(message -> {
long ping = System.currentTimeMillis() - startTime;
long gatewayPing = context.getMessage().getJDA().getGatewayPing();
message.editMessageEmbeds(
EmbedFactory.create()
.setTitle("🏓 Pong!")
.setColor(Color.GREEN)
.addField("Задержка API", ping + "ms", true)
.addField("Задержка Gateway", gatewayPing + "ms", true)
.build()
).queue();
});
}
}

View file

@ -0,0 +1,103 @@
package creator.loki.command.impl.utility;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import java.awt.Color;
public class PollCommand implements Command {
private static final String[] EMOJI_NUMBERS = {
"1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"
};
@Override
public String getName() {
return "poll";
}
@Override
public String getDescription() {
return "Создаёт голосование с реакциями";
}
@Override
public String getUsage() {
return "poll <вопрос> | <вариант1> | <вариант2> ...";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
if (context.getArgs().isEmpty()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"Неверное использование",
"Используй: `!poll <вопрос> | <вариант1> | <вариант2> ...`\n" +
"Пример: `!poll Какой язык лучше? | Java | Python | JavaScript`"
)
).queue();
return;
}
// Объединяем все аргументы и разделяем по |
String fullText = String.join(" ", context.getArgs());
String[] parts = fullText.split("\\|");
if (parts.length < 3) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"Недостаточно вариантов",
"Укажите вопрос и минимум 2 варианта ответа, разделённых символом `|`"
)
).queue();
return;
}
if (parts.length > 11) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"Слишком много вариантов",
"Максимум 10 вариантов ответа"
)
).queue();
return;
}
String question = parts[0].trim();
String[] options = new String[parts.length - 1];
for (int i = 1; i < parts.length; i++) {
options[i - 1] = parts[i].trim();
}
// Формируем описание с вариантами
StringBuilder description = new StringBuilder();
for (int i = 0; i < options.length; i++) {
description.append(EMOJI_NUMBERS[i])
.append(" ")
.append(options[i])
.append("\n");
}
// Создаём embed
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("📊 " + question)
.setDescription(description.toString())
.setColor(Color.ORANGE)
.setFooter("Голосование от " + context.getAuthor().getUser().getName())
.build()
).queue(message -> {
// Добавляем реакции
for (int i = 0; i < options.length; i++) {
message.addReaction(Emoji.fromUnicode(EMOJI_NUMBERS[i])).queue();
}
});
}
}

View file

@ -0,0 +1,88 @@
package creator.loki.command.impl.utility;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.data.JsonDataStore;
import creator.loki.profile.ProfileData;
import creator.loki.profile.ProfileGenerator;
import creator.loki.profile.ProfileService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.utils.FileUpload;
import java.util.List;
/**
* Команда для отображения профиля пользователя
*/
public class ProfileCommand implements Command {
private final ProfileService profileService;
private final ProfileGenerator profileGenerator;
public ProfileCommand(JsonDataStore dataStore) {
this.profileService = new ProfileService(dataStore);
this.profileGenerator = new ProfileGenerator();
}
@Override
public String getName() {
return "profile";
}
@Override
public String getDescription() {
return "Показать профиль пользователя";
}
@Override
public String getUsage() {
return "profile [@user]";
}
@Override
public String getCategory() {
return "Общие";
}
@Override
public void execute(CommandContext context) {
if (context.getGuild() == null) {
context.getChannel().sendMessage("Команда доступна только на сервере").queue();
return;
}
// Определяем целевого пользователя
Member target;
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
target = context.getAuthor();
} else {
target = mentioned.get(0);
}
if (target == null) {
context.getChannel().sendMessage("Пользователь не найден").queue();
return;
}
// Отправляем сообщение о генерации
context.getChannel().sendMessage("🎨 Генерирую профиль...").queue(loadingMsg -> {
try {
// Получаем данные профиля
ProfileData profileData = profileService.getProfileData(target, context.getGuild());
// Генерируем изображение
byte[] imageBytes = profileGenerator.generateProfile(profileData);
// Отправляем изображение
loadingMsg.delete().queue();
context.getChannel().sendFiles(
FileUpload.fromData(imageBytes, "profile.png")
).queue();
} catch (Exception e) {
loadingMsg.editMessage("❌ Ошибка генерации профиля: " + e.getMessage()).queue();
e.printStackTrace();
}
});
}
}

View file

@ -0,0 +1,138 @@
package creator.loki.command.impl.utility;
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.Guild;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
public class SwapCommand implements Command {
private final Random random = new Random();
@Override
public String getName() {
return "swap";
}
@Override
public String getDescription() {
return "Перекидывает пользователя между случайными голосовыми каналами";
}
@Override
public String getUsage() {
return "swap <@user> <количество 1-5>";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
if (context.getGuild() == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Команда доступна только на сервере")
).queue();
return;
}
// Проверка прав
if (context.getAuthor() == null || !context.getAuthor().hasPermission(Permission.VOICE_MOVE_OTHERS)) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Недостаточно прав", "У тебя нет прав на перемещение пользователей")
).queue();
return;
}
if (!context.getGuild().getSelfMember().hasPermission(Permission.VOICE_MOVE_OTHERS)) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Недостаточно прав", "У бота нет прав на перемещение пользователей")
).queue();
return;
}
// Проверка аргументов
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty() || context.getArgs().size() < 2) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Используй: `!swap <@user> <количество 1-5>`")
).queue();
return;
}
Member target = mentioned.get(0);
int swapCount;
try {
swapCount = Integer.parseInt(context.getArgs().get(1));
if (swapCount < 1 || swapCount > 5) {
throw new NumberFormatException();
}
} catch (NumberFormatException e) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Количество должно быть от 1 до 5")
).queue();
return;
}
// Проверка, что пользователь в войсе
GuildVoiceState voiceState = target.getVoiceState();
if (voiceState == null || !voiceState.inAudioChannel()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", target.getUser().getName() + " не в голосовом канале")
).queue();
return;
}
VoiceChannel originalChannel = voiceState.getChannel().asVoiceChannel();
Guild guild = context.getGuild();
// Получаем все голосовые каналы кроме текущего
List<VoiceChannel> voiceChannels = guild.getVoiceChannels().stream()
.filter(vc -> !vc.getId().equals(originalChannel.getId()))
.collect(Collectors.toList());
if (voiceChannels.isEmpty()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Недостаточно голосовых каналов для swap")
).queue();
return;
}
context.getChannel().sendMessageEmbeds(
EmbedTemplates.success(
"🔄 Swap запущен!",
"Перекидываю " + target.getAsMention() + " между каналами " + swapCount + " раз(а)"
)
).queue();
// Запускаем swap в отдельном потоке
new Thread(() -> {
try {
for (int i = 0; i < swapCount; i++) {
// Перекидываем в случайный канал
VoiceChannel randomChannel = voiceChannels.get(random.nextInt(voiceChannels.size()));
guild.moveVoiceMember(target, randomChannel).queue();
Thread.sleep(800);
// Возвращаем обратно
guild.moveVoiceMember(target, originalChannel).queue();
Thread.sleep(800);
}
} catch (Exception e) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Не удалось выполнить swap: " + e.getMessage())
).queue();
}
}).start();
}
}

View file

@ -0,0 +1,106 @@
package creator.loki.command.impl.utility;
import creator.loki.audio.AudioManager;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedTemplates;
import creator.loki.util.CooldownManager;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
public class TtsCommand implements Command {
private final CooldownManager cooldownManager;
private final AudioManager audioManager;
public TtsCommand(CooldownManager cooldownManager, AudioManager audioManager) {
this.cooldownManager = cooldownManager;
this.audioManager = audioManager;
}
@Override
public String getName() {
return "tts";
}
@Override
public String getDescription() {
return "Бот заходит в голосовой канал и произносит текст";
}
@Override
public String getUsage() {
return "tts <текст>";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
if (context.getGuild() == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Команда доступна только на сервере")
).queue();
return;
}
if (context.getArgs().isEmpty()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажи текст для TTS")
).queue();
return;
}
Member member = context.getAuthor();
if (member == null) return;
// Проверка cooldown (15 секунд)
if (cooldownManager.isOnCooldown(member.getId(), "tts")) {
long remaining = cooldownManager.getRemainingSeconds(member.getId(), "tts");
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"Подожди!",
"Ты сможешь использовать TTS через " + remaining + " сек."
)
).queue();
return;
}
// Проверка, что пользователь в голосовом канале
GuildVoiceState voiceState = member.getVoiceState();
if (voiceState == null || !voiceState.inAudioChannel()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Ты должен быть в голосовом канале")
).queue();
return;
}
VoiceChannel voiceChannel = voiceState.getChannel().asVoiceChannel();
String text = String.join(" ", context.getArgs());
if (text.length() > 200) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Слишком длинно", "Максимум 200 символов")
).queue();
return;
}
// Подключаемся к голосовому каналу (если ещё не подключены)
net.dv8tion.jda.api.managers.AudioManager audioManagerJda = context.getGuild().getAudioManager();
if (!audioManagerJda.isConnected()) {
audioManagerJda.openAudioConnection(voiceChannel);
}
// Добавляем TTS в очередь
audioManager.enqueueTts(context.getGuild(), text, context.getChannel());
cooldownManager.applyCooldown(member.getId(), "tts", 15);
context.getChannel().sendMessageEmbeds(
EmbedTemplates.success("TTS", "🔊 Добавлено в очередь: " + text)
).queue();
}
}

View file

@ -0,0 +1,50 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import java.awt.Color;
/**
* Slash команда /avatar
*/
public class AvatarSlashCommand implements SlashCommand {
@Override
public void execute(SlashCommandInteractionEvent event) {
User targetUser = event.getOption("user") != null
? event.getOption("user").getAsUser()
: event.getUser();
String avatarUrl = targetUser.getEffectiveAvatarUrl() + "?size=512";
EmbedBuilder embed = new EmbedBuilder()
.setTitle("🖼️ Аватар " + targetUser.getName())
.setImage(avatarUrl)
.setColor(new Color(88, 101, 242))
.setFooter("Запросил: " + event.getUser().getName());
event.replyEmbeds(embed.build()).queue();
}
@Override
public CommandData getCommandData() {
return Commands.slash("avatar", "Показывает аватар пользователя")
.addOption(OptionType.USER, "user", "Пользователь (по умолчанию - вы)", false);
}
@Override
public String getName() {
return "avatar";
}
@Override
public String getDescription() {
return "Показывает аватар пользователя";
}
}

View file

@ -0,0 +1,103 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
public class BanSlashCommand implements SlashCommand {
@Override
public void execute(SlashCommandInteractionEvent event) {
if (event.getGuild() == null) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Команда доступна только на сервере")).setEphemeral(true).queue();
return;
}
Member author = event.getMember();
if (author == null || !author.hasPermission(Permission.BAN_MEMBERS)) {
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "У вас нет прав на бан пользователей")).setEphemeral(true).queue();
return;
}
if (!event.getGuild().getSelfMember().hasPermission(Permission.BAN_MEMBERS)) {
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "У бота нет прав на бан пользователей")).setEphemeral(true).queue();
return;
}
Member target = event.getOption("user").getAsMember();
if (target == null) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Пользователь не найден")).setEphemeral(true).queue();
return;
}
if (target.getId().equals(author.getId())) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Вы не можете забанить самого себя")).setEphemeral(true).queue();
return;
}
if (target.getUser().isBot() && target.getId().equals(event.getGuild().getSelfMember().getId())) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Я не могу забанить самого себя")).setEphemeral(true).queue();
return;
}
if (!author.canInteract(target)) {
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "Вы не можете забанить этого пользователя")).setEphemeral(true).queue();
return;
}
if (!event.getGuild().getSelfMember().canInteract(target)) {
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "Я не могу забанить этого пользователя")).setEphemeral(true).queue();
return;
}
String reason = event.getOption("reason") != null
? event.getOption("reason").getAsString()
: "Причина не указана";
int deleteDays = event.getOption("delete_days") != null
? event.getOption("delete_days").getAsInt()
: 0;
event.deferReply().queue();
event.getGuild().ban(target, deleteDays, java.util.concurrent.TimeUnit.DAYS)
.reason(reason)
.queue(
success -> event.getHook().editOriginalEmbeds(
EmbedTemplates.success(
"Пользователь забанен",
"**Пользователь:** " + target.getUser().getAsTag() + "\n" +
"**Причина:** " + reason + "\n" +
"**Удалено сообщений за дней:** " + deleteDays + "\n" +
"**Модератор:** " + author.getAsMention()
)
).queue(),
error -> event.getHook().editOriginalEmbeds(
EmbedTemplates.error("Ошибка", "Не удалось забанить: " + error.getMessage())
).queue()
);
}
@Override
public CommandData getCommandData() {
return Commands.slash("ban", "Банит пользователя на сервере")
.addOption(OptionType.USER, "user", "Пользователь для бана", true)
.addOption(OptionType.STRING, "reason", "Причина бана", false)
.addOption(OptionType.INTEGER, "delete_days", "Удалить сообщения за N дней (0-7)", false);
}
@Override
public String getName() {
return "ban";
}
@Override
public String getDescription() {
return "Банит пользователя на сервере";
}
}

View file

@ -0,0 +1,74 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
public class ClearSlashCommand implements SlashCommand {
@Override
public void execute(SlashCommandInteractionEvent event) {
if (event.getGuild() == null) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Команда доступна только на сервере")).setEphemeral(true).queue();
return;
}
Member author = event.getMember();
if (author == null || !author.hasPermission(Permission.MESSAGE_MANAGE)) {
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "У вас нет прав на управление сообщениями")).setEphemeral(true).queue();
return;
}
if (!event.getGuild().getSelfMember().hasPermission(Permission.MESSAGE_MANAGE)) {
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "У бота нет прав на управление сообщениями")).setEphemeral(true).queue();
return;
}
int amount = event.getOption("amount").getAsInt();
if (amount < 1 || amount > 100) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Количество должно быть от 1 до 100")).setEphemeral(true).queue();
return;
}
event.deferReply(true).queue();
TextChannel channel = event.getChannel().asTextChannel();
channel.getHistory().retrievePast(amount).queue(
messages -> {
channel.purgeMessages(messages);
event.getHook().editOriginalEmbeds(
EmbedTemplates.success(
"Сообщения удалены",
"Удалено сообщений: " + messages.size()
)
).queue();
},
error -> event.getHook().editOriginalEmbeds(
EmbedTemplates.error("Ошибка", "Не удалось удалить сообщения: " + error.getMessage())
).queue()
);
}
@Override
public CommandData getCommandData() {
return Commands.slash("clear", "Удаляет указанное количество сообщений")
.addOption(OptionType.INTEGER, "amount", "Количество сообщений (1-100)", true);
}
@Override
public String getName() {
return "clear";
}
@Override
public String getDescription() {
return "Удаляет указанное количество сообщений";
}
}

View file

@ -0,0 +1,42 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import java.awt.Color;
import java.util.Random;
public class CoinSlashCommand implements SlashCommand {
private static final Random random = new Random();
@Override
public void execute(SlashCommandInteractionEvent event) {
boolean isHeads = random.nextBoolean();
String result = isHeads ? "Орёл 🦅" : "Решка 🪙";
EmbedBuilder embed = new EmbedBuilder()
.setTitle("🪙 Подбрасывание монетки")
.setDescription("Результат: **" + result + "**")
.setColor(isHeads ? new Color(255, 215, 0) : new Color(192, 192, 192));
event.replyEmbeds(embed.build()).queue();
}
@Override
public CommandData getCommandData() {
return Commands.slash("coin", "Подбросить монетку");
}
@Override
public String getName() {
return "coin";
}
@Override
public String getDescription() {
return "Подбросить монетку";
}
}

View file

@ -0,0 +1,54 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import java.awt.Color;
import java.util.Random;
public class DiceSlashCommand implements SlashCommand {
private static final Random random = new Random();
@Override
public void execute(SlashCommandInteractionEvent event) {
int result = random.nextInt(6) + 1;
String emoji = getDiceEmoji(result);
EmbedBuilder embed = new EmbedBuilder()
.setTitle("🎲 Бросок кубика")
.setDescription("Выпало: **" + result + "** " + emoji)
.setColor(new Color(88, 101, 242));
event.replyEmbeds(embed.build()).queue();
}
private String getDiceEmoji(int number) {
return switch (number) {
case 1 -> "⚀";
case 2 -> "⚁";
case 3 -> "⚂";
case 4 -> "⚃";
case 5 -> "⚄";
case 6 -> "⚅";
default -> "🎲";
};
}
@Override
public CommandData getCommandData() {
return Commands.slash("dice", "Бросить кубик");
}
@Override
public String getName() {
return "dice";
}
@Override
public String getDescription() {
return "Бросить кубик";
}
}

View file

@ -0,0 +1,73 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import java.awt.Color;
import java.util.Random;
public class EightBallSlashCommand implements SlashCommand {
private static final Random random = new Random();
private static final String[] ANSWERS = {
"Бесспорно ✅",
"Предрешено ✅",
"Никаких сомнений ✅",
"Определённо да ✅",
"Можешь быть уверен в этом ✅",
"Мне кажется — да ✅",
"Вероятнее всего ✅",
"Хорошие перспективы ✅",
"Знаки говорят — да ✅",
"Да ✅",
"Пока не ясно 🤔",
"Спроси позже 🤔",
"Лучше не рассказывать 🤔",
"Сейчас нельзя предсказать 🤔",
"Сконцентрируйся и спроси опять 🤔",
"Даже не думай ❌",
"Мой ответ — нет ❌",
"По моим данным — нет ❌",
"Перспективы не очень хорошие ❌",
"Весьма сомнительно ❌"
};
@Override
public void execute(SlashCommandInteractionEvent event) {
var questionOption = event.getOption("question");
if (questionOption == null) {
event.reply("Задайте вопрос!").setEphemeral(true).queue();
return;
}
String question = questionOption.getAsString();
String answer = ANSWERS[random.nextInt(ANSWERS.length)];
EmbedBuilder embed = new EmbedBuilder()
.setTitle("🎱 Магический шар")
.addField("Вопрос", question, false)
.addField("Ответ", answer, false)
.setColor(new Color(88, 101, 242));
event.replyEmbeds(embed.build()).queue();
}
@Override
public CommandData getCommandData() {
return Commands.slash("8ball", "Задать вопрос магическому шару")
.addOption(OptionType.STRING, "question", "Ваш вопрос", true);
}
@Override
public String getName() {
return "8ball";
}
@Override
public String getDescription() {
return "Задать вопрос магическому шару";
}
}

View file

@ -0,0 +1,48 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import java.awt.Color;
public class HugSlashCommand implements SlashCommand {
@Override
public void execute(SlashCommandInteractionEvent event) {
var targetOption = event.getOption("user");
if (targetOption == null) {
event.reply("Укажите пользователя!").setEphemeral(true).queue();
return;
}
var target = targetOption.getAsUser();
var author = event.getUser();
EmbedBuilder embed = new EmbedBuilder()
.setDescription(author.getAsMention() + " обнимает " + target.getAsMention() + " 🤗")
.setColor(new Color(255, 182, 193))
.setImage("https://media.tenor.com/images/hug.gif");
event.replyEmbeds(embed.build()).queue();
}
@Override
public CommandData getCommandData() {
return Commands.slash("hug", "Обнять пользователя")
.addOption(OptionType.USER, "user", "Кого обнять", true);
}
@Override
public String getName() {
return "hug";
}
@Override
public String getDescription() {
return "Обнять пользователя";
}
}

View file

@ -0,0 +1,97 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
public class KickSlashCommand implements SlashCommand {
@Override
public void execute(SlashCommandInteractionEvent event) {
if (event.getGuild() == null) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Команда доступна только на сервере")).setEphemeral(true).queue();
return;
}
Member author = event.getMember();
if (author == null || !author.hasPermission(Permission.KICK_MEMBERS)) {
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "У вас нет прав на кик пользователей")).setEphemeral(true).queue();
return;
}
if (!event.getGuild().getSelfMember().hasPermission(Permission.KICK_MEMBERS)) {
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "У бота нет прав на кик пользователей")).setEphemeral(true).queue();
return;
}
Member target = event.getOption("user").getAsMember();
if (target == null) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Пользователь не найден")).setEphemeral(true).queue();
return;
}
if (target.getId().equals(author.getId())) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Вы не можете кикнуть самого себя")).setEphemeral(true).queue();
return;
}
if (target.getUser().isBot() && target.getId().equals(event.getGuild().getSelfMember().getId())) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Я не могу кикнуть самого себя")).setEphemeral(true).queue();
return;
}
if (!author.canInteract(target)) {
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "Вы не можете кикнуть этого пользователя")).setEphemeral(true).queue();
return;
}
if (!event.getGuild().getSelfMember().canInteract(target)) {
event.replyEmbeds(EmbedTemplates.error("Недостаточно прав", "Я не могу кикнуть этого пользователя")).setEphemeral(true).queue();
return;
}
String reason = event.getOption("reason") != null
? event.getOption("reason").getAsString()
: "Причина не указана";
event.deferReply().queue();
event.getGuild().kick(target)
.reason(reason)
.queue(
success -> event.getHook().editOriginalEmbeds(
EmbedTemplates.success(
"Пользователь кикнут",
"**Пользователь:** " + target.getUser().getAsTag() + "\n" +
"**Причина:** " + reason + "\n" +
"**Модератор:** " + author.getAsMention()
)
).queue(),
error -> event.getHook().editOriginalEmbeds(
EmbedTemplates.error("Ошибка", "Не удалось кикнуть: " + error.getMessage())
).queue()
);
}
@Override
public CommandData getCommandData() {
return Commands.slash("kick", "Кикает пользователя с сервера")
.addOption(OptionType.USER, "user", "Пользователь для кика", true)
.addOption(OptionType.STRING, "reason", "Причина кика", false);
}
@Override
public String getName() {
return "kick";
}
@Override
public String getDescription() {
return "Кикает пользователя с сервера";
}
}

View file

@ -0,0 +1,58 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import creator.loki.metrics.MetricsCollector;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import java.awt.Color;
/**
* Slash команда /metrics
*/
public class MetricsSlashCommand implements SlashCommand {
private final MetricsCollector metrics;
public MetricsSlashCommand(MetricsCollector metrics) {
this.metrics = metrics;
}
@Override
public void execute(SlashCommandInteractionEvent event) {
// Только администраторы
if (event.getMember() == null || !event.getMember().hasPermission(Permission.ADMINISTRATOR)) {
event.reply("❌ Эта команда доступна только администраторам.")
.setEphemeral(true)
.queue();
return;
}
String stats = metrics.getFullStats();
EmbedBuilder embed = new EmbedBuilder()
.setTitle("📊 Метрики бота")
.setDescription("```\n" + stats + "\n```")
.setColor(new Color(88, 101, 242))
.setFooter("Slash команда");
event.replyEmbeds(embed.build()).setEphemeral(true).queue();
}
@Override
public CommandData getCommandData() {
return Commands.slash("metrics", "Показывает метрики и статистику бота (только для админов)");
}
@Override
public String getName() {
return "metrics";
}
@Override
public String getDescription() {
return "Показывает метрики и статистику бота";
}
}

View file

@ -0,0 +1,50 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import java.awt.Color;
/**
* Slash команда /ping
*/
public class PingSlashCommand implements SlashCommand {
@Override
public void execute(SlashCommandInteractionEvent event) {
JDA jda = event.getJDA();
long gatewayPing = jda.getGatewayPing();
event.deferReply().queue(hook -> {
long restPing = hook.getInteraction().getJDA().getRestPing().complete();
EmbedBuilder embed = new EmbedBuilder()
.setTitle("🏓 Pong!")
.setColor(new Color(88, 101, 242))
.addField("Gateway Ping", gatewayPing + " ms", true)
.addField("REST Ping", restPing + " ms", true)
.setFooter("Slash команда");
hook.editOriginalEmbeds(embed.build()).queue();
});
}
@Override
public CommandData getCommandData() {
return Commands.slash("ping", "Проверяет задержку бота");
}
@Override
public String getName() {
return "ping";
}
@Override
public String getDescription() {
return "Проверяет задержку бота";
}
}

View file

@ -0,0 +1,79 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import creator.loki.data.JsonDataStore;
import creator.loki.level.LevelSystem;
import creator.loki.profile.ProfileData;
import creator.loki.profile.ProfileService;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import java.awt.Color;
/**
* Slash команда /profile
*/
public class ProfileSlashCommand implements SlashCommand {
private final ProfileService profileService;
public ProfileSlashCommand(JsonDataStore dataStore) {
this.profileService = new ProfileService(dataStore);
}
@Override
public void execute(SlashCommandInteractionEvent event) {
Member targetMember = event.getOption("user") != null
? event.getOption("user").getAsMember()
: event.getMember();
if (targetMember == null) {
event.reply("❌ Не удалось получить информацию о пользователе.")
.setEphemeral(true)
.queue();
return;
}
ProfileData profile = profileService.getProfileData(targetMember, event.getGuild());
// Создаём прогресс-бар
int progress = (int) ((double) profile.getCurrentXp() / profile.getNextLevelXp() * 20);
String progressBar = "▰".repeat(progress) + "▱".repeat(20 - progress);
EmbedBuilder embed = new EmbedBuilder()
.setTitle("📊 Профиль " + profile.getUsername())
.setThumbnail(profile.getAvatarUrl())
.setColor(new Color(138, 80, 255))
.addField("🎯 Уровень", String.valueOf(profile.getLevel()), true)
.addField("⭐ Ранг", "#" + profile.getRank(), true)
.addField("💬 Сообщений", String.valueOf(profile.getMessagesCount()), true)
.addField("🎤 Голос (мин)", String.valueOf(profile.getVoiceMinutes()), true)
.addField("👍 Репутация", String.valueOf(profile.getReputation()), true)
.addField("📈 Прогресс",
progressBar + "\n" + profile.getCurrentXp() + " / " + profile.getNextLevelXp() + " XP",
false)
.setFooter("Запросил: " + event.getUser().getName());
event.replyEmbeds(embed.build()).queue();
}
@Override
public CommandData getCommandData() {
return Commands.slash("profile", "Показывает профиль пользователя")
.addOption(OptionType.USER, "user", "Пользователь (по умолчанию - вы)", false);
}
@Override
public String getName() {
return "profile";
}
@Override
public String getDescription() {
return "Показывает профиль пользователя";
}
}

View file

@ -0,0 +1,62 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import creator.loki.data.JsonDataStore;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import java.awt.Color;
import java.util.Map;
public class RankSlashCommand implements SlashCommand {
private final JsonDataStore dataStore;
public RankSlashCommand(JsonDataStore dataStore) {
this.dataStore = dataStore;
}
@Override
public void execute(SlashCommandInteractionEvent event) {
String userId = event.getUser().getId();
String levelKey = "level_" + userId;
String xpKey = "xp_" + userId;
Integer level = dataStore.getData(levelKey, Integer.class);
Integer xp = dataStore.getData(xpKey, Integer.class);
if (level == null) level = 1;
if (xp == null) xp = 0;
int xpNeeded = calculateXpForNextLevel(level);
EmbedBuilder embed = new EmbedBuilder()
.setTitle("📊 Ваш ранг")
.setDescription(event.getUser().getAsMention())
.addField("Уровень", String.valueOf(level), true)
.addField("Опыт", xp + " / " + xpNeeded, true)
.setColor(new Color(88, 101, 242))
.setThumbnail(event.getUser().getAvatarUrl());
event.replyEmbeds(embed.build()).queue();
}
private int calculateXpForNextLevel(int level) {
return 100 * level;
}
@Override
public CommandData getCommandData() {
return Commands.slash("rank", "Показать ваш текущий ранг и уровень");
}
@Override
public String getName() {
return "rank";
}
@Override
public String getDescription() {
return "Показать ваш текущий ранг и уровень";
}
}

View file

@ -0,0 +1,70 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import creator.loki.data.JsonDataStore;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import java.awt.Color;
public class RepSlashCommand implements SlashCommand {
private final JsonDataStore dataStore;
public RepSlashCommand(JsonDataStore dataStore) {
this.dataStore = dataStore;
}
@Override
public void execute(SlashCommandInteractionEvent event) {
var targetOption = event.getOption("user");
if (targetOption == null) {
event.reply("Укажите пользователя!").setEphemeral(true).queue();
return;
}
var target = targetOption.getAsUser();
var author = event.getUser();
if (target.getId().equals(author.getId())) {
event.reply("Вы не можете дать репутацию самому себе!").setEphemeral(true).queue();
return;
}
if (target.isBot()) {
event.reply("Нельзя давать репутацию ботам!").setEphemeral(true).queue();
return;
}
String key = "rep_" + target.getId();
Integer currentRep = dataStore.getData(key, Integer.class);
if (currentRep == null) currentRep = 0;
dataStore.setData(key, currentRep + 1);
EmbedBuilder embed = new EmbedBuilder()
.setTitle("✨ Репутация")
.setDescription(author.getAsMention() + " дал репутацию " + target.getAsMention())
.addField("Новая репутация", String.valueOf(currentRep + 1), false)
.setColor(new Color(0, 255, 0));
event.replyEmbeds(embed.build()).queue();
}
@Override
public CommandData getCommandData() {
return Commands.slash("rep", "Дать репутацию пользователю")
.addOption(OptionType.USER, "user", "Кому дать репутацию", true);
}
@Override
public String getName() {
return "rep";
}
@Override
public String getDescription() {
return "Дать репутацию пользователю";
}
}

View file

@ -0,0 +1,80 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import java.awt.Color;
import java.util.Random;
public class ShipSlashCommand implements SlashCommand {
private static final Random random = new Random();
@Override
public void execute(SlashCommandInteractionEvent event) {
var user1Option = event.getOption("user1");
var user2Option = event.getOption("user2");
if (user1Option == null || user2Option == null) {
event.reply("Укажите двух пользователей!").setEphemeral(true).queue();
return;
}
var user1 = user1Option.getAsUser();
var user2 = user2Option.getAsUser();
int percentage = random.nextInt(101);
String hearts = getHearts(percentage);
String comment = getComment(percentage);
EmbedBuilder embed = new EmbedBuilder()
.setTitle("💕 Ship-метр")
.setDescription(
user1.getAsMention() + " ❤️ " + user2.getAsMention() + "\n\n" +
hearts + " **" + percentage + "%**\n\n" +
comment
)
.setColor(getColor(percentage));
event.replyEmbeds(embed.build()).queue();
}
private String getHearts(int percentage) {
int hearts = percentage / 10;
return "❤️".repeat(hearts) + "🖤".repeat(10 - hearts);
}
private String getComment(int percentage) {
if (percentage >= 90) return "Идеальная пара! 💖";
if (percentage >= 70) return "Отличная совместимость! 💕";
if (percentage >= 50) return "Неплохие шансы! 💗";
if (percentage >= 30) return "Есть потенциал... 💔";
return "Лучше остаться друзьями 💔";
}
private Color getColor(int percentage) {
if (percentage >= 70) return new Color(255, 105, 180);
if (percentage >= 40) return new Color(255, 182, 193);
return new Color(128, 128, 128);
}
@Override
public CommandData getCommandData() {
return Commands.slash("ship", "Проверить совместимость двух пользователей")
.addOption(OptionType.USER, "user1", "Первый пользователь", true)
.addOption(OptionType.USER, "user2", "Второй пользователь", true);
}
@Override
public String getName() {
return "ship";
}
@Override
public String getDescription() {
return "Проверить совместимость двух пользователей";
}
}

View file

@ -0,0 +1,64 @@
package creator.loki.command.slash;
import creator.loki.command.SlashCommand;
import creator.loki.audio.AudioManager;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
public class TtsSlashCommand implements SlashCommand {
private final AudioManager audioManager;
public TtsSlashCommand(AudioManager audioManager) {
this.audioManager = audioManager;
}
@Override
public void execute(SlashCommandInteractionEvent event) {
if (event.getGuild() == null) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Команда доступна только на сервере")).setEphemeral(true).queue();
return;
}
Member member = event.getMember();
if (member == null) {
event.reply("Ошибка получения информации о пользователе").setEphemeral(true).queue();
return;
}
GuildVoiceState voiceState = member.getVoiceState();
if (voiceState == null || !voiceState.inAudioChannel()) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Вы должны находиться в голосовом канале")).setEphemeral(true).queue();
return;
}
String text = event.getOption("text").getAsString();
if (text.length() > 200) {
event.reply("Текст слишком длинный! Максимум 200 символов.").setEphemeral(true).queue();
return;
}
event.reply("🔊 Воспроизведение TTS...").queue();
audioManager.enqueueTts(event.getGuild(), text, event.getChannel());
}
@Override
public CommandData getCommandData() {
return Commands.slash("tts", "Озвучить текст в голосовом канале")
.addOption(OptionType.STRING, "text", "Текст для озвучки (макс. 200 символов)", true);
}
@Override
public String getName() {
return "tts";
}
@Override
public String getDescription() {
return "Озвучить текст в голосовом канале";
}
}

View file

@ -0,0 +1,25 @@
package creator.loki.config;
public class BotConfig {
private final String token;
private final String prefix;
private final String activity;
public BotConfig(String token, String prefix, String activity) {
this.token = token;
this.prefix = prefix;
this.activity = activity;
}
public String getToken() {
return token;
}
public String getPrefix() {
return prefix;
}
public String getActivity() {
return activity;
}
}

View file

@ -0,0 +1,39 @@
package creator.loki.config;
import io.github.cdimascio.dotenv.Dotenv;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ConfigLoader {
private static final Logger logger = LoggerFactory.getLogger(ConfigLoader.class);
private ConfigLoader() {
// Запрет создания экземпляров
}
public static BotConfig load() {
Dotenv dotenv = Dotenv.configure()
.ignoreIfMissing()
.load();
String token = dotenv.get("DISCORD_BOT_TOKEN");
if (token == null || token.isBlank()) {
throw new IllegalStateException(
"Токен бота не найден! Установите переменную окружения DISCORD_BOT_TOKEN или создайте .env файл"
);
}
String prefix = dotenv.get("BOT_PREFIX");
if (prefix == null || prefix.isBlank()) {
prefix = "!";
}
String activity = dotenv.get("BOT_ACTIVITY");
if (activity == null || activity.isBlank()) {
activity = "Используй !help";
}
logger.info("Конфигурация загружена успешно");
return new BotConfig(token, prefix, activity);
}
}

View file

@ -0,0 +1,232 @@
package creator.loki.core;
import com.google.inject.Inject;
import creator.loki.audio.AudioManager;
import creator.loki.command.impl.setup.CounterSetupCommand;
import creator.loki.command.impl.setup.LogSetupCommand;
import creator.loki.command.impl.setup.VoiceSetupCommand;
import creator.loki.command.impl.stats.LeaderboardButtonListener;
import creator.loki.config.BotConfig;
import creator.loki.counter.CounterManager;
import creator.loki.data.JsonDataStore;
import creator.loki.database.ConfigRepository;
import creator.loki.database.Database;
import creator.loki.level.LevelSystem;
import creator.loki.listener.*;
import creator.loki.metrics.MetricsCollector;
import creator.loki.profile.ProfileService;
import creator.loki.role.CustomRoleManager;
import creator.loki.util.CooldownManager;
import creator.loki.util.GlobalRateLimiter;
import creator.loki.voice.VoiceChannelManager;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.requests.GatewayIntent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BotInitializer {
private static final Logger logger = LoggerFactory.getLogger(BotInitializer.class);
private final BotConfig config;
private final CommandRegistry registry;
private final SlashCommandRegistry slashRegistry;
private final EventDispatcher dispatcher;
private final JsonDataStore dataStore;
private final CustomRoleManager roleManager;
private final CooldownManager cooldownManager;
private final GlobalRateLimiter rateLimiter;
private final MetricsCollector metrics;
private final AudioManager audioManager;
private final LevelSystem levelSystem;
private final VoiceTracker voiceTracker;
private final ProfileService profileService;
private final VoiceChannelManager voiceChannelManager;
private final CounterManager counterManager;
private final VoiceChannelListener voiceChannelListener;
private final MemberCounterListener memberCounterListener;
private final Database database;
private final ConfigRepository configRepo;
private final VoiceSetupCommand voiceSetupCommand;
private final CounterSetupCommand counterSetupCommand;
private final LogSetupCommand logSetupCommand;
private final VoiceButtonHandler voiceButtonHandler;
private final ModerationLogger moderationLogger;
private final LevelUpNotifier levelUpNotifier;
@Inject
public BotInitializer(
BotConfig config,
CommandRegistry registry,
SlashCommandRegistry slashRegistry,
EventDispatcher dispatcher,
JsonDataStore dataStore,
CustomRoleManager roleManager,
CooldownManager cooldownManager,
GlobalRateLimiter rateLimiter,
MetricsCollector metrics,
AudioManager audioManager,
LevelSystem levelSystem,
VoiceTracker voiceTracker,
ProfileService profileService,
VoiceChannelManager voiceChannelManager,
CounterManager counterManager,
Database database,
ConfigRepository configRepo,
LevelUpNotifier levelUpNotifier
) {
this.config = config;
this.registry = registry;
this.slashRegistry = slashRegistry;
this.dispatcher = dispatcher;
this.dataStore = dataStore;
this.roleManager = roleManager;
this.cooldownManager = cooldownManager;
this.rateLimiter = rateLimiter;
this.metrics = metrics;
this.audioManager = audioManager;
this.levelSystem = levelSystem;
this.voiceTracker = voiceTracker;
this.profileService = profileService;
this.voiceChannelManager = voiceChannelManager;
this.counterManager = counterManager;
this.database = database;
this.configRepo = configRepo;
this.levelUpNotifier = levelUpNotifier;
this.voiceChannelListener = new VoiceChannelListener(voiceChannelManager);
this.memberCounterListener = new MemberCounterListener(counterManager);
this.voiceSetupCommand = new VoiceSetupCommand(configRepo);
this.counterSetupCommand = new CounterSetupCommand(configRepo);
this.logSetupCommand = new LogSetupCommand(configRepo);
this.voiceButtonHandler = new VoiceButtonHandler(voiceChannelManager);
this.moderationLogger = new ModerationLogger(configRepo);
}
public JDA start() {
logger.info("Инициализация бота...");
// Регистрируем команды через специализированные регистраторы
registerCommands();
registerSlashCommands();
// Создаём JDA
JDA jda = createJDA();
// Регистрируем slash команды в Discord
registerSlashCommandsInDiscord(jda);
// Настраиваем graceful shutdown
setupShutdownHook(jda);
return jda;
}
private void registerCommands() {
PrefixCommandRegistrar registrar = new PrefixCommandRegistrar(
registry, config, dataStore, roleManager, cooldownManager,
audioManager, levelSystem, metrics
);
registrar.registerAll();
}
private void registerSlashCommands() {
SlashCommandRegistrar registrar = new SlashCommandRegistrar(
slashRegistry, dataStore, metrics, audioManager
);
registrar.registerAll();
}
private JDA createJDA() {
return JDABuilder.createDefault(config.getToken())
.setStatus(OnlineStatus.ONLINE)
.setActivity(Activity.playing(config.getActivity()))
.enableIntents(
GatewayIntent.GUILD_MESSAGES,
GatewayIntent.MESSAGE_CONTENT,
GatewayIntent.GUILD_MEMBERS,
GatewayIntent.GUILD_VOICE_STATES
)
.addEventListeners(
new ReadyListener(),
new MessageListener(dispatcher, dataStore, levelSystem),
new SlashCommandListener(slashRegistry, rateLimiter, metrics),
voiceTracker,
voiceChannelListener,
memberCounterListener,
voiceSetupCommand,
counterSetupCommand,
logSetupCommand,
voiceButtonHandler,
moderationLogger,
new LeaderboardButtonListener(levelSystem)
)
.build();
}
private void registerSlashCommandsInDiscord(JDA jda) {
try {
jda.awaitReady();
slashRegistry.registerCommandsInDiscord(jda);
// Регистрируем setup команды
VoiceSetupCommand.register(jda);
CounterSetupCommand.register(jda);
LogSetupCommand.register(jda);
// Запускаем планировщик счетчика участников
if (!jda.getGuilds().isEmpty()) {
memberCounterListener.startScheduler(jda.getGuilds().get(0));
}
} catch (InterruptedException e) {
logger.error("Прервано ожидание готовности JDA", e);
Thread.currentThread().interrupt();
}
}
private void setupShutdownHook(JDA jda) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("========================================");
logger.info("Получен сигнал завершения работы бота...");
logger.info("========================================");
shutdownComponents(jda);
logger.info("========================================");
logger.info("Бот успешно завершён!");
logger.info("========================================");
}, "ShutdownHook"));
}
private void shutdownComponents(JDA jda) {
try {
logger.info("Сохранение данных...");
dataStore.flush();
logger.info("Остановка менеджеров...");
cooldownManager.shutdown();
rateLimiter.shutdown();
voiceTracker.shutdown();
memberCounterListener.shutdown();
dispatcher.shutdown();
audioManager.shutdown();
logger.info("Завершение JsonDataStore...");
dataStore.shutdown();
logger.info("Закрытие базы данных...");
database.close();
logger.info("Отключение JDA...");
jda.shutdown();
if (!jda.awaitShutdown(java.time.Duration.ofSeconds(10))) {
logger.warn("JDA не завершился за 10 секунд, принудительная остановка");
jda.shutdownNow();
}
} catch (Exception e) {
logger.error("Ошибка при завершении работы бота", e);
}
}
}

View file

@ -0,0 +1,32 @@
package creator.loki.core;
import creator.loki.command.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class CommandRegistry {
private static final Logger logger = LoggerFactory.getLogger(CommandRegistry.class);
private final Map<String, Command> commands = new HashMap<>();
public void register(Command command) {
commands.put(command.getName().toLowerCase(), command);
logger.debug("Зарегистрирована команда: {}", command.getName());
}
public Command getCommand(String name) {
return commands.get(name.toLowerCase());
}
public Collection<Command> getAllCommands() {
return Collections.unmodifiableCollection(commands.values());
}
public int getCommandCount() {
return commands.size();
}
}

View file

@ -0,0 +1,129 @@
package creator.loki.core;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.embed.EmbedTemplates;
import creator.loki.exception.CommandExceptionHandler;
import creator.loki.metrics.MetricsCollector;
import creator.loki.util.GlobalRateLimiter;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class EventDispatcher {
private static final Logger logger = LoggerFactory.getLogger(EventDispatcher.class);
private final BotConfig config;
private final CommandRegistry registry;
private final ExecutorService executorService;
private final GlobalRateLimiter rateLimiter;
private final MetricsCollector metrics;
public EventDispatcher(BotConfig config, CommandRegistry registry, GlobalRateLimiter rateLimiter, MetricsCollector metrics) {
this.config = config;
this.registry = registry;
this.executorService = Executors.newCachedThreadPool();
this.rateLimiter = rateLimiter;
this.metrics = metrics;
}
public void handleMessage(MessageReceivedEvent event) {
// Игнорируем ботов
if (event.getAuthor().isBot()) {
return;
}
String content = event.getMessage().getContentRaw();
if (!content.startsWith(config.getPrefix())) {
return;
}
// Парсим команду и аргументы
String[] parts = content.substring(config.getPrefix().length()).trim().split("\\s+");
if (parts.length == 0) {
return;
}
String commandName = parts[0].toLowerCase();
List<String> args = parts.length > 1
? Arrays.asList(Arrays.copyOfRange(parts, 1, parts.length))
: List.of();
Command command = registry.getCommand(commandName);
if (command == null) {
event.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"Команда не найдена",
"Команда `" + commandName + "` не существует. Используй `" + config.getPrefix() + "help`"
)
).queue();
return;
}
// Проверяем глобальный rate limit
String userId = event.getAuthor().getId();
String guildId = event.getGuild() != null ? event.getGuild().getId() : "DM";
if (!rateLimiter.allow(userId, guildId)) {
long waitTime = rateLimiter.getUserWaitTime(userId);
event.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"⏱️ Слишком быстро!",
"Ты отправляешь команды слишком часто. Подожди " + waitTime + " секунд."
)
).queue();
logger.warn("Rate limit для пользователя {} в гильдии {}", userId, guildId);
return;
}
// Выполняем команду асинхронно
executorService.submit(() -> {
long startTime = System.currentTimeMillis();
try {
CommandContext context = new CommandContext(
event.getMessage(),
args,
event.getChannel(),
event.getMember(),
event.getGuild()
);
command.execute(context);
// Записываем метрики
long executionTime = System.currentTimeMillis() - startTime;
metrics.recordCommand(commandName, executionTime);
logger.info("Выполнена команда: {} от {} за {} мс",
commandName, event.getAuthor().getName(), executionTime);
} catch (Exception e) {
// Записываем ошибку в метрики
metrics.recordError(commandName);
// Используем централизованный обработчик ошибок
CommandExceptionHandler.handle(e, event.getChannel(), commandName);
}
});
}
public void shutdown() {
logger.info("Завершение работы EventDispatcher...");
executorService.shutdown();
try {
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
logger.warn("ExecutorService не завершился за 10 секунд, принудительная остановка");
executorService.shutdownNow();
}
} catch (InterruptedException e) {
logger.error("Прервано ожидание завершения ExecutorService", e);
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
logger.info("EventDispatcher завершён");
}
}

View file

@ -0,0 +1,128 @@
package creator.loki.core;
import creator.loki.audio.AudioManager;
import creator.loki.command.impl.entertainment.games.*;
import creator.loki.command.impl.entertainment.interaction.*;
import creator.loki.command.impl.moderation.*;
import creator.loki.command.impl.roles.*;
import creator.loki.command.impl.stats.*;
import creator.loki.command.impl.utility.*;
import creator.loki.config.BotConfig;
import creator.loki.data.JsonDataStore;
import creator.loki.level.LevelSystem;
import creator.loki.metrics.MetricsCollector;
import creator.loki.profile.ProfileService;
import creator.loki.role.CustomRoleManager;
import creator.loki.util.CooldownManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Регистратор префиксных команд (!) - Single Responsibility
*/
public class PrefixCommandRegistrar {
private static final Logger logger = LoggerFactory.getLogger(PrefixCommandRegistrar.class);
private final CommandRegistry registry;
private final BotConfig config;
private final JsonDataStore dataStore;
private final CustomRoleManager roleManager;
private final CooldownManager cooldownManager;
private final AudioManager audioManager;
private final LevelSystem levelSystem;
private final MetricsCollector metrics;
public PrefixCommandRegistrar(
CommandRegistry registry,
BotConfig config,
JsonDataStore dataStore,
CustomRoleManager roleManager,
CooldownManager cooldownManager,
AudioManager audioManager,
LevelSystem levelSystem,
MetricsCollector metrics
) {
this.registry = registry;
this.config = config;
this.dataStore = dataStore;
this.roleManager = roleManager;
this.cooldownManager = cooldownManager;
this.audioManager = audioManager;
this.levelSystem = levelSystem;
this.metrics = metrics;
}
public void registerAll() {
registerUtilityCommands();
registerRoleCommands();
registerInteractionCommands();
registerGameCommands();
registerStatsCommands();
registerModerationCommands();
logger.info("Зарегистрировано префиксных команд: {}", registry.getCommandCount());
}
private void registerUtilityCommands() {
registry.register(new PingCommand());
registry.register(new HelpCommand(registry, config));
registry.register(new InfoCommand());
registry.register(new ClearCommand());
registry.register(new ProfileCommand(dataStore));
registry.register(new MetricsCommand(metrics));
registry.register(new AvatarCommand());
registry.register(new PollCommand());
registry.register(new SwapCommand());
registry.register(new TtsCommand(cooldownManager, audioManager));
}
private void registerRoleCommands() {
registry.register(new CreateRoleCommand(roleManager));
registry.register(new DeleteRoleCommand(roleManager));
registry.register(new MyRolesCommand(roleManager));
registry.register(new EditRoleCommand(roleManager));
}
private void registerInteractionCommands() {
registry.register(new PatCommand(cooldownManager));
registry.register(new HugCommand());
registry.register(new KissCommand(cooldownManager));
registry.register(new SlapCommand(cooldownManager));
registry.register(new TickleCommand(cooldownManager));
registry.register(new PokeCommand(cooldownManager));
registry.register(new CuddleCommand(cooldownManager));
registry.register(new FeedCommand(cooldownManager));
}
private void registerGameCommands() {
registry.register(new EightBallCommand());
registry.register(new CoinCommand());
registry.register(new DiceCommand());
registry.register(new MemeCommand());
registry.register(new ShipCommand(dataStore));
registry.register(new RoastCommand());
registry.register(new ComplimentCommand());
registry.register(new GlazbobaCommand());
registry.register(new IqCommand(cooldownManager));
registry.register(new GayCommand(cooldownManager));
registry.register(new PpCommand(cooldownManager));
registry.register(new RateCommand(cooldownManager));
registry.register(new BoykisserCommand(cooldownManager));
registry.register(new MustacheCommand(cooldownManager));
registry.register(new FatCommand(cooldownManager));
}
private void registerStatsCommands() {
registry.register(new RankCommand(levelSystem));
registry.register(new NewLeaderboardCommand(levelSystem));
registry.register(new RepCommand(dataStore, cooldownManager));
registry.register(new MinusRepCommand(dataStore, cooldownManager));
registry.register(new RepTopCommand(dataStore));
registry.register(new StatsCommand());
}
private void registerModerationCommands() {
registry.register(new BanCommand());
registry.register(new KickCommand());
}
}

View file

@ -0,0 +1,59 @@
package creator.loki.core;
import creator.loki.audio.AudioManager;
import creator.loki.command.slash.*;
import creator.loki.data.JsonDataStore;
import creator.loki.metrics.MetricsCollector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Регистратор slash команд (/) - Single Responsibility
*/
public class SlashCommandRegistrar {
private static final Logger logger = LoggerFactory.getLogger(SlashCommandRegistrar.class);
private final creator.loki.core.SlashCommandRegistry registry;
private final JsonDataStore dataStore;
private final MetricsCollector metrics;
private final AudioManager audioManager;
public SlashCommandRegistrar(
creator.loki.core.SlashCommandRegistry registry,
JsonDataStore dataStore,
MetricsCollector metrics,
AudioManager audioManager
) {
this.registry = registry;
this.dataStore = dataStore;
this.metrics = metrics;
this.audioManager = audioManager;
}
public void registerAll() {
// Утилиты
registry.register(new PingSlashCommand());
registry.register(new AvatarSlashCommand());
registry.register(new ProfileSlashCommand(dataStore));
registry.register(new MetricsSlashCommand(metrics));
registry.register(new TtsSlashCommand(audioManager));
// Модерация
registry.register(new KickSlashCommand());
registry.register(new BanSlashCommand());
registry.register(new ClearSlashCommand());
// Статистика
registry.register(new RepSlashCommand(dataStore));
registry.register(new RankSlashCommand(dataStore));
// Развлечения
registry.register(new HugSlashCommand());
registry.register(new ShipSlashCommand());
registry.register(new CoinSlashCommand());
registry.register(new DiceSlashCommand());
registry.register(new EightBallSlashCommand());
logger.info("Зарегистрировано slash команд: {}", registry.getCommandCount());
}
}

View file

@ -0,0 +1,67 @@
package creator.loki.core;
import creator.loki.command.SlashCommand;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Реестр для slash команд
*/
public class SlashCommandRegistry {
private static final Logger logger = LoggerFactory.getLogger(SlashCommandRegistry.class);
private final Map<String, SlashCommand> commands = new HashMap<>();
/**
* Регистрирует slash команду
*/
public void register(SlashCommand command) {
commands.put(command.getName().toLowerCase(), command);
logger.debug("Зарегистрирована slash команда: {}", command.getName());
}
/**
* Получает slash команду по имени
*/
public SlashCommand getCommand(String name) {
return commands.get(name.toLowerCase());
}
/**
* Возвращает все slash команды
*/
public Collection<SlashCommand> getAllCommands() {
return Collections.unmodifiableCollection(commands.values());
}
/**
* Возвращает количество slash команд
*/
public int getCommandCount() {
return commands.size();
}
/**
* Регистрирует все slash команды в Discord
*/
public void registerCommandsInDiscord(JDA jda) {
List<CommandData> commandDataList = new ArrayList<>();
for (SlashCommand command : commands.values()) {
commandDataList.add(command.getCommandData());
}
jda.updateCommands().addCommands(commandDataList).queue(
success -> logger.info("Зарегистрировано {} slash команд в Discord", commandDataList.size()),
error -> logger.error("Ошибка при регистрации slash команд", error)
);
}
}

View file

@ -0,0 +1,226 @@
package creator.loki.counter;
import com.google.gson.reflect.TypeToken;
import creator.loki.data.JsonDataStore;
import creator.loki.database.ConfigRepository;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.util.*;
/**
* Менеджер счетчика участников
*/
public class CounterManager {
private static final Logger logger = LoggerFactory.getLogger(CounterManager.class);
private final JsonDataStore dataStore;
private final ConfigRepository configRepo;
private String counterChannelId;
private String format = "【👥】Участники: {count}";
public CounterManager(JsonDataStore dataStore, ConfigRepository configRepo) {
this.dataStore = dataStore;
this.configRepo = configRepo;
loadConfig();
}
/**
* Загрузить конфигурацию
*/
private void loadConfig() {
this.counterChannelId = configRepo.get("counter.channel_id");
String savedFormat = configRepo.get("counter.format");
if (savedFormat != null) {
this.format = savedFormat;
}
}
/**
* Получить данные счетчика
*/
@SuppressWarnings("unchecked")
private Map<String, Object> getCounterData() {
Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> data = dataStore.getData("counter_data", type);
return data != null ? data : new HashMap<>();
}
/**
* Сохранить данные счетчика
*/
private void saveCounterData(Map<String, Object> data) {
dataStore.setData("counter_data", data);
}
/**
* Установить канал счетчика
*/
public void setCounterChannel(String channelId) {
configRepo.set("counter.channel_id", channelId);
this.counterChannelId = channelId;
logger.info("Установлен канал счетчика: {}", channelId);
}
/**
* Установить формат отображения
*/
public void setFormat(String format) {
configRepo.set("counter.format", format);
this.format = format;
logger.info("Установлен формат: {}", format);
}
/**
* Получить текущую дневную статистику
*/
@SuppressWarnings("unchecked")
public DailyStats getDailyStats() {
Map<String, Object> data = getCounterData();
Map<String, Object> statsMap = (Map<String, Object>) data.get("daily_stats");
if (statsMap == null) {
return new DailyStats();
}
DailyStats stats = new DailyStats();
stats.setDate((String) statsMap.getOrDefault("date", LocalDate.now().toString()));
stats.setJoined(((Number) statsMap.getOrDefault("joined", 0)).intValue());
stats.setLeft(((Number) statsMap.getOrDefault("left", 0)).intValue());
// Если статистика не сегодняшняя, создаем новую
if (!stats.isToday()) {
// Сохраняем старую в историю
saveToHistory(stats);
stats = new DailyStats();
}
return stats;
}
/**
* Сохранить статистику
*/
private void saveDailyStats(DailyStats stats) {
Map<String, Object> data = getCounterData();
Map<String, Object> statsMap = new HashMap<>();
statsMap.put("date", stats.getDate());
statsMap.put("joined", stats.getJoined());
statsMap.put("left", stats.getLeft());
statsMap.put("difference", stats.getDifference());
data.put("daily_stats", statsMap);
saveCounterData(data);
}
/**
* Сохранить статистику в историю
*/
@SuppressWarnings("unchecked")
private void saveToHistory(DailyStats stats) {
Map<String, Object> data = getCounterData();
List<Map<String, Object>> history = (List<Map<String, Object>>) data.get("history");
if (history == null) {
history = new ArrayList<>();
}
Map<String, Object> statsMap = new HashMap<>();
statsMap.put("date", stats.getDate());
statsMap.put("joined", stats.getJoined());
statsMap.put("left", stats.getLeft());
statsMap.put("difference", stats.getDifference());
history.add(statsMap);
// Храним только последние 30 дней
if (history.size() > 30) {
history.remove(0);
}
data.put("history", history);
saveCounterData(data);
}
/**
* Зарегистрировать присоединение участника
*/
public void recordJoin() {
DailyStats stats = getDailyStats();
stats.incrementJoined();
saveDailyStats(stats);
logger.debug("Зарегистрировано присоединение. Всего за день: {}", stats.getJoined());
}
/**
* Зарегистрировать выход участника
*/
public void recordLeave() {
DailyStats stats = getDailyStats();
stats.incrementLeft();
saveDailyStats(stats);
logger.debug("Зарегистрирован выход. Всего за день: {}", stats.getLeft());
}
/**
* Обновить счетчик участников
*/
public void updateCounter(Guild guild) {
if (counterChannelId == null || counterChannelId.isEmpty()) {
return;
}
VoiceChannel channel = guild.getVoiceChannelById(counterChannelId);
if (channel == null) {
logger.warn("Канал счетчика не найден: {}", counterChannelId);
return;
}
int memberCount = guild.getMemberCount();
String newName = format.replace("{count}", String.valueOf(memberCount));
// Обновляем название канала только если оно изменилось
if (!channel.getName().equals(newName)) {
channel.getManager().setName(newName).queue(
success -> logger.debug("Обновлен счетчик участников: {}", memberCount),
error -> logger.error("Ошибка при обновлении счетчика", error)
);
}
}
/**
* Получить историю статистики
*/
@SuppressWarnings("unchecked")
public List<DailyStats> getHistory(int days) {
Map<String, Object> data = getCounterData();
List<Map<String, Object>> historyMaps = (List<Map<String, Object>>) data.get("history");
if (historyMaps == null) {
return new ArrayList<>();
}
List<DailyStats> history = new ArrayList<>();
int start = Math.max(0, historyMaps.size() - days);
for (int i = start; i < historyMaps.size(); i++) {
Map<String, Object> statsMap = historyMaps.get(i);
DailyStats stats = new DailyStats();
stats.setDate((String) statsMap.get("date"));
stats.setJoined(((Number) statsMap.get("joined")).intValue());
stats.setLeft(((Number) statsMap.get("left")).intValue());
history.add(stats);
}
return history;
}
// Getters
public String getCounterChannelId() { return counterChannelId; }
public String getFormat() { return format; }
}

View file

@ -0,0 +1,66 @@
package creator.loki.counter;
import java.time.LocalDate;
/**
* Дневная статистика участников
*/
public class DailyStats {
private String date;
private int joined;
private int left;
private int difference;
public DailyStats() {
this.date = LocalDate.now().toString();
this.joined = 0;
this.left = 0;
this.difference = 0;
}
public DailyStats(String date, int joined, int left) {
this.date = date;
this.joined = joined;
this.left = left;
this.difference = joined - left;
}
// Getters
public String getDate() { return date; }
public int getJoined() { return joined; }
public int getLeft() { return left; }
public int getDifference() { return difference; }
// Setters
public void setDate(String date) { this.date = date; }
public void setJoined(int joined) {
this.joined = joined;
updateDifference();
}
public void setLeft(int left) {
this.left = left;
updateDifference();
}
// Методы
public void incrementJoined() {
this.joined++;
updateDifference();
}
public void incrementLeft() {
this.left++;
updateDifference();
}
private void updateDifference() {
this.difference = this.joined - this.left;
}
/**
* Проверить является ли статистика сегодняшней
*/
public boolean isToday() {
return this.date.equals(LocalDate.now().toString());
}
}

View file

@ -0,0 +1,67 @@
package creator.loki.database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* Репозиторий для работы с конфигурацией
*/
public class ConfigRepository {
private static final Logger logger = LoggerFactory.getLogger(ConfigRepository.class);
private final Database database;
public ConfigRepository(Database database) {
this.database = database;
}
/**
* Получить значение конфигурации
*/
public String get(String key) {
try (PreparedStatement stmt = database.prepare("SELECT value FROM config WHERE key = ?")) {
stmt.setString(1, key);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getString("value");
}
return null;
} catch (SQLException e) {
logger.error("Ошибка получения конфигурации: {}", key, e);
return null;
}
}
/**
* Установить значение конфигурации
*/
public void set(String key, String value) {
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)"
)) {
stmt.setString(1, key);
stmt.setString(2, value);
stmt.executeUpdate();
logger.info("Конфигурация обновлена: {} = {}", key, value);
} catch (SQLException e) {
logger.error("Ошибка сохранения конфигурации: {}", key, e);
}
}
/**
* Удалить значение конфигурации
*/
public void delete(String key) {
try (PreparedStatement stmt = database.prepare("DELETE FROM config WHERE key = ?")) {
stmt.setString(1, key);
stmt.executeUpdate();
logger.info("Конфигурация удалена: {}", key);
} catch (SQLException e) {
logger.error("Ошибка удаления конфигурации: {}", key, e);
}
}
}

View file

@ -0,0 +1,92 @@
package creator.loki.database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
/**
* Менеджер SQLite базы данных
*/
public class Database {
private static final Logger logger = LoggerFactory.getLogger(Database.class);
private final String dbPath;
private Connection connection;
public Database(String dbPath) {
this.dbPath = dbPath;
}
/**
* Подключиться к базе данных
*/
public void connect() {
try {
String url = "jdbc:sqlite:" + dbPath;
connection = DriverManager.getConnection(url);
logger.info("Подключение к базе данных: {}", dbPath);
// Включаем foreign keys
try (Statement stmt = connection.createStatement()) {
stmt.execute("PRAGMA foreign_keys = ON");
}
} catch (SQLException e) {
logger.error("Ошибка подключения к базе данных", e);
throw new RuntimeException(e);
}
}
/**
* Получить соединение
*/
public Connection getConnection() {
try {
if (connection == null || connection.isClosed()) {
connect();
}
return connection;
} catch (SQLException e) {
logger.error("Ошибка проверки соединения", e);
connect();
return connection;
}
}
/**
* Выполнить SQL запрос
*/
public void execute(String sql) {
try (Statement stmt = getConnection().createStatement()) {
stmt.execute(sql);
} catch (SQLException e) {
logger.error("Ошибка выполнения SQL: {}", sql, e);
throw new RuntimeException(e);
}
}
/**
* Выполнить SQL запрос с параметрами
*/
public PreparedStatement prepare(String sql) {
try {
return getConnection().prepareStatement(sql);
} catch (SQLException e) {
logger.error("Ошибка подготовки SQL: {}", sql, e);
throw new RuntimeException(e);
}
}
/**
* Закрыть соединение
*/
public void close() {
try {
if (connection != null && !connection.isClosed()) {
connection.close();
logger.info("Соединение с базой данных закрыто");
}
} catch (SQLException e) {
logger.error("Ошибка закрытия соединения", e);
}
}
}

View file

@ -0,0 +1,191 @@
package creator.loki.database;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileReader;
import java.lang.reflect.Type;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
/**
* Миграция данных из JSON в SQLite
*/
public class DatabaseMigration {
private static final Logger logger = LoggerFactory.getLogger(DatabaseMigration.class);
private final Database database;
private final Gson gson = new Gson();
public DatabaseMigration(Database database) {
this.database = database;
}
/**
* Мигрировать данные из JSON
*/
public void migrateFromJson(String jsonPath) {
logger.info("Начало миграции данных из JSON: {}", jsonPath);
try (FileReader reader = new FileReader(jsonPath)) {
Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> data = gson.fromJson(reader, type);
if (data == null) {
logger.warn("JSON файл пустой или не существует");
return;
}
if (isMigrated()) {
logger.info("Данные уже мигрированы, пропускаем");
return;
}
migrateUserStats(data);
migrateReputation(data);
migrateShipPairs(data);
migrateCustomRoles(data);
markMigrated();
logger.info("Миграция завершена успешно");
} catch (Exception e) {
logger.error("Ошибка миграции данных", e);
}
}
private boolean isMigrated() {
try (PreparedStatement stmt = database.prepare("SELECT value FROM config WHERE key = 'migrated'")) {
ResultSet rs = stmt.executeQuery();
return rs.next() && "true".equals(rs.getString("value"));
} catch (SQLException e) {
return false;
}
}
private void markMigrated() {
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO config (key, value) VALUES ('migrated', 'true')"
)) {
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Ошибка сохранения статуса миграции", e);
}
}
@SuppressWarnings("unchecked")
private void migrateUserStats(Map<String, Object> data) {
Map<String, Object> userStats = (Map<String, Object>) data.get("userStats");
if (userStats == null) return;
logger.info("Миграция userStats: {} записей", userStats.size());
String sql = """
INSERT OR REPLACE INTO user_stats
(user_id, level, total_xp, message_count, voice_minutes, last_message_time, last_voice_join)
VALUES (?, ?, ?, ?, ?, ?, ?)
""";
try (PreparedStatement stmt = database.prepare(sql)) {
for (Map.Entry<String, Object> entry : userStats.entrySet()) {
Map<String, Object> stats = (Map<String, Object>) entry.getValue();
stmt.setString(1, (String) stats.get("userId"));
stmt.setInt(2, ((Number) stats.get("level")).intValue());
stmt.setInt(3, ((Number) stats.get("totalXP")).intValue());
stmt.setInt(4, ((Number) stats.get("messageCount")).intValue());
stmt.setLong(5, ((Number) stats.get("voiceMinutes")).longValue());
stmt.setLong(6, ((Number) stats.get("lastMessageTime")).longValue());
stmt.setLong(7, ((Number) stats.get("lastVoiceJoin")).longValue());
stmt.executeUpdate();
}
logger.info("userStats мигрированы");
} catch (SQLException e) {
logger.error("Ошибка миграции userStats", e);
}
}
@SuppressWarnings("unchecked")
private void migrateReputation(Map<String, Object> data) {
Map<String, Object> reputation = (Map<String, Object>) data.get("reputation");
if (reputation == null) return;
logger.info("Миграция reputation: {} записей", reputation.size());
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO reputation (user_id, reputation) VALUES (?, ?)"
)) {
for (Map.Entry<String, Object> entry : reputation.entrySet()) {
stmt.setString(1, entry.getKey());
stmt.setDouble(2, ((Number) entry.getValue()).doubleValue());
stmt.executeUpdate();
}
logger.info("reputation мигрирована");
} catch (SQLException e) {
logger.error("Ошибка миграции reputation", e);
}
}
@SuppressWarnings("unchecked")
private void migrateShipPairs(Map<String, Object> data) {
Map<String, Object> shipPairs = (Map<String, Object>) data.get("shipPairs");
if (shipPairs == null) return;
logger.info("Миграция shipPairs: {} записей", shipPairs.size());
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO ship_pairs (user1_id, user2_id, compatibility) VALUES (?, ?, ?)"
)) {
for (Map.Entry<String, Object> entry : shipPairs.entrySet()) {
String[] users = entry.getKey().split("-");
if (users.length == 2) {
stmt.setString(1, users[0]);
stmt.setString(2, users[1]);
stmt.setDouble(3, ((Number) entry.getValue()).doubleValue());
stmt.executeUpdate();
}
}
logger.info("shipPairs мигрированы");
} catch (SQLException e) {
logger.error("Ошибка миграции shipPairs", e);
}
}
@SuppressWarnings("unchecked")
private void migrateCustomRoles(Map<String, Object> data) {
Map<String, Object> customRoles = (Map<String, Object>) data.get("custom_roles");
if (customRoles == null) return;
logger.info("Миграция custom_roles");
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO custom_roles (guild_id, user_id, role_id) VALUES (?, ?, ?)"
)) {
for (Map.Entry<String, Object> guildEntry : customRoles.entrySet()) {
String guildId = guildEntry.getKey();
Map<String, Object> users = (Map<String, Object>) guildEntry.getValue();
for (Map.Entry<String, Object> userEntry : users.entrySet()) {
String userId = userEntry.getKey();
Object rolesObj = userEntry.getValue();
if (rolesObj instanceof java.util.List) {
for (Object roleId : (java.util.List<?>) rolesObj) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.setString(3, roleId.toString());
stmt.executeUpdate();
}
}
}
}
logger.info("custom_roles мигрированы");
} catch (SQLException e) {
logger.error("Ошибка миграции custom_roles", e);
}
}
}

View file

@ -0,0 +1,112 @@
package creator.loki.database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Схема базы данных - создание таблиц
*/
public class DatabaseSchema {
private static final Logger logger = LoggerFactory.getLogger(DatabaseSchema.class);
private final Database database;
public DatabaseSchema(Database database) {
this.database = database;
}
/**
* Создать все таблицы
*/
public void createTables() {
logger.info("Создание таблиц базы данных...");
createConfigTable();
createUserStatsTable();
createReputationTable();
createShipPairsTable();
createCustomRolesTable();
createVoiceChannelsTable();
createCounterStatsTable();
logger.info("Таблицы созданы успешно");
}
private void createConfigTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
""");
}
private void createUserStatsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS user_stats (
user_id TEXT PRIMARY KEY,
level INTEGER DEFAULT 0,
total_xp INTEGER DEFAULT 0,
message_count INTEGER DEFAULT 0,
voice_minutes INTEGER DEFAULT 0,
last_message_time INTEGER DEFAULT 0,
last_voice_join INTEGER DEFAULT 0
)
""");
}
private void createReputationTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS reputation (
user_id TEXT PRIMARY KEY,
reputation REAL DEFAULT 0.0
)
""");
}
private void createShipPairsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS ship_pairs (
user1_id TEXT NOT NULL,
user2_id TEXT NOT NULL,
compatibility REAL NOT NULL,
PRIMARY KEY (user1_id, user2_id)
)
""");
}
private void createCustomRolesTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS custom_roles (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
role_id TEXT NOT NULL,
PRIMARY KEY (guild_id, user_id, role_id)
)
""");
}
private void createVoiceChannelsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS voice_channels (
channel_id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
name TEXT,
user_limit INTEGER DEFAULT 0,
bitrate INTEGER DEFAULT 64000,
locked INTEGER DEFAULT 0
)
""");
}
private void createCounterStatsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS counter_stats (
date TEXT PRIMARY KEY,
joined INTEGER DEFAULT 0,
left_count INTEGER DEFAULT 0,
difference INTEGER DEFAULT 0
)
""");
}
}

View file

@ -0,0 +1,133 @@
package creator.loki.di;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import creator.loki.audio.AudioManager;
import creator.loki.config.BotConfig;
import creator.loki.config.ConfigLoader;
import creator.loki.core.CommandRegistry;
import creator.loki.core.EventDispatcher;
import creator.loki.core.SlashCommandRegistry;
import creator.loki.data.JsonDataStore;
import creator.loki.database.ConfigRepository;
import creator.loki.database.Database;
import creator.loki.database.DatabaseMigration;
import creator.loki.database.DatabaseSchema;
import creator.loki.level.LevelSystem;
import creator.loki.listener.LevelUpNotifier;
import creator.loki.listener.VoiceTracker;
import creator.loki.metrics.MetricsCollector;
import creator.loki.profile.ProfileService;
import creator.loki.role.CustomRoleManager;
import creator.loki.util.CooldownManager;
import creator.loki.util.GlobalRateLimiter;
import creator.loki.voice.VoiceChannelManager;
import creator.loki.counter.CounterManager;
/**
* Главный модуль Guice для конфигурации Dependency Injection
*/
public class BotModule extends AbstractModule {
@Override
protected void configure() {
// Все основные компоненты - Singleton
bind(CommandRegistry.class).in(Singleton.class);
bind(SlashCommandRegistry.class).in(Singleton.class);
bind(CooldownManager.class).in(Singleton.class);
bind(GlobalRateLimiter.class).in(Singleton.class);
bind(MetricsCollector.class).in(Singleton.class);
bind(AudioManager.class).in(Singleton.class);
}
@Provides
@Singleton
BotConfig provideBotConfig() {
return ConfigLoader.load();
}
@Provides
@Singleton
JsonDataStore provideJsonDataStore() {
JsonDataStore dataStore = new JsonDataStore("data/bot_data.json");
dataStore.load();
return dataStore;
}
@Provides
@Singleton
CustomRoleManager provideCustomRoleManager(JsonDataStore dataStore) {
return new CustomRoleManager(dataStore);
}
@Provides
@Singleton
LevelSystem provideLevelSystem(JsonDataStore dataStore) {
return new LevelSystem(dataStore);
}
@Provides
@Singleton
VoiceTracker provideVoiceTracker(LevelSystem levelSystem) {
return new VoiceTracker(levelSystem);
}
@Provides
@Singleton
ProfileService provideProfileService(JsonDataStore dataStore) {
return new ProfileService(dataStore);
}
@Provides
@Singleton
EventDispatcher provideEventDispatcher(
BotConfig config,
CommandRegistry registry,
GlobalRateLimiter rateLimiter,
MetricsCollector metrics
) {
return new EventDispatcher(config, registry, rateLimiter, metrics);
}
@Provides
@Singleton
Database provideDatabase() {
Database database = new Database("data/bot.db");
database.connect();
// Создаем таблицы
DatabaseSchema schema = new DatabaseSchema(database);
schema.createTables();
// Мигрируем данные из JSON если нужно
DatabaseMigration migration = new DatabaseMigration(database);
migration.migrateFromJson("data/bot_data.json");
return database;
}
@Provides
@Singleton
ConfigRepository provideConfigRepository(Database database) {
return new ConfigRepository(database);
}
@Provides
@Singleton
LevelUpNotifier provideLevelUpNotifier(ConfigRepository configRepo, LevelSystem levelSystem) {
return new LevelUpNotifier(configRepo, levelSystem);
}
@Provides
@Singleton
VoiceChannelManager provideVoiceChannelManager(JsonDataStore dataStore, ConfigRepository configRepo) {
return new VoiceChannelManager(dataStore, configRepo);
}
@Provides
@Singleton
CounterManager provideCounterManager(JsonDataStore dataStore, ConfigRepository configRepo) {
return new CounterManager(dataStore, configRepo);
}
}

Some files were not shown because too many files have changed in this diff Show more