feat: economy system with minigames, bank, rob, jobs, fishing, blackjack, slots
+ economy domain (EconomyAccount, Job, SlotCombination, EconomyTransaction) + EconomyRepository + SqlEconomyRepository + DB schema + EconomyService (transfer, daily, deposit/withdraw, stat upgrades) + FishingService (fish grid with difficulty levels, speed bonus) + BlackjackService (dealer stands on 16, ties lose) + SlotService (weighted combos up to x50) + JobService (6 jobs with level reqs and efficiency stat) + 11 commands: balance, baltop, pay, daily, bank, rob, fish, blackjack, slot, job, work + Tests for SqlEconomyRepository + EconomyModule + EconomyCommandRegistrar wired into BotModule
This commit is contained in:
parent
fcb5cd7207
commit
fe121c8546
270 changed files with 18632 additions and 11656 deletions
13
.dockerignore
Normal file
13
.dockerignore
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
.env
|
||||||
|
.gradle
|
||||||
|
build
|
||||||
|
bin
|
||||||
|
out
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.md
|
||||||
|
logs
|
||||||
|
data
|
||||||
12
.editorconfig
Normal file
12
.editorconfig
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
6
.env.example
Normal file
6
.env.example
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
DISCORD_BOT_TOKEN=your_token_here
|
||||||
|
BOT_PREFIX=!
|
||||||
|
BOT_ACTIVITY=Используй !help
|
||||||
|
HEALTH_HTTP_PORT=8712
|
||||||
|
SUPER_ADMIN_ID=your_discord_user_id
|
||||||
|
DB_PATH=data/bot.db
|
||||||
19
.github/workflows/ci.yml
vendored
Normal file
19
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: "21"
|
||||||
|
distribution: "temurin"
|
||||||
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
|
- run: ./gradlew build
|
||||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
FROM eclipse-temurin:21-jdk-alpine AS builder
|
||||||
|
WORKDIR /build
|
||||||
|
COPY gradle gradle
|
||||||
|
COPY gradlew settings.gradle build.gradle ./
|
||||||
|
COPY src src
|
||||||
|
RUN ./gradlew jar --no-daemon --stacktrace
|
||||||
|
|
||||||
|
FROM eclipse-temurin:21-jre-alpine
|
||||||
|
RUN addgroup -S bot && adduser -S bot -G bot
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /build/build/libs/discord-bot-1.0.0.jar app.jar
|
||||||
|
COPY --from=builder /build/data/ ./data/
|
||||||
|
RUN mkdir -p /app/data && chown -R bot:bot /app
|
||||||
|
USER bot
|
||||||
|
VOLUME /app/data
|
||||||
|
EXPOSE 8712
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8712/health || exit 1
|
||||||
|
ENV DB_PATH=/app/data/bot.db
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
145
FEATURES.md
145
FEATURES.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## 📋 Описание
|
## 📋 Описание
|
||||||
|
|
||||||
Многофункциональный Discord бот с системой уровней, личными голосовыми каналами, модерацией и развлекательными командами.
|
Многофункциональный Discord бот с системой уровней, личными голосовыми каналами, модерацией и развлекательными командами. **Все команды — префиксные** (ниже для краткости префикс `!`; реальный префикс задаётся в конфиге бота).
|
||||||
|
|
||||||
**Технологии:**
|
**Технологии:**
|
||||||
- Java 21
|
- Java 21
|
||||||
|
|
@ -20,8 +20,8 @@
|
||||||
- 10 XP за сообщение (cooldown 60 сек)
|
- 10 XP за сообщение (cooldown 60 сек)
|
||||||
- 1 XP за минуту в войсе
|
- 1 XP за минуту в войсе
|
||||||
- **Команды:**
|
- **Команды:**
|
||||||
- `/rank [@user]` - карточка ранга
|
- `!rank [@user]` — карточка ранга
|
||||||
- `/leaderboard` - топ по уровням/сообщениям/войсу
|
- `!leaderboard` — топ по уровням/сообщениям/войсу
|
||||||
- **Уведомления:** Красивые embed при повышении уровня
|
- **Уведомления:** Красивые embed при повышении уровня
|
||||||
- **Награды:** Роли за достижение уровней (5, 10, 20, 30, 50)
|
- **Награды:** Роли за достижение уровней (5, 10, 20, 30, 50)
|
||||||
|
|
||||||
|
|
@ -34,14 +34,14 @@
|
||||||
- 🎨 Битрейт
|
- 🎨 Битрейт
|
||||||
- 🚫 Удалить
|
- 🚫 Удалить
|
||||||
- **Автоудаление:** Пустые каналы удаляются автоматически
|
- **Автоудаление:** Пустые каналы удаляются автоматически
|
||||||
- **Настройка:** `/voice-setup`
|
- **Настройка:** `!voice-setup`
|
||||||
|
|
||||||
### 📊 Счетчик участников
|
### 📊 Счетчик участников
|
||||||
- **Автообновление:** Название канала с количеством участников
|
- **Автообновление:** Название канала с количеством участников
|
||||||
- **Формат:** `【👥】Участники: 123`
|
- **Формат:** `【👥】Участники: 123`
|
||||||
- **Статистика:** Присоединения/выходы за день
|
- **Статистика:** Присоединения/выходы за день
|
||||||
- **История:** 30 дней
|
- **История:** 30 дней
|
||||||
- **Настройка:** `/counter-setup`
|
- **Настройка:** `!counter-setup`
|
||||||
|
|
||||||
### 📝 Логи модерации
|
### 📝 Логи модерации
|
||||||
- **Что логируется:**
|
- **Что логируется:**
|
||||||
|
|
@ -50,48 +50,47 @@
|
||||||
- 👢 Кики
|
- 👢 Кики
|
||||||
- 🔇 Муты/размуты
|
- 🔇 Муты/размуты
|
||||||
- 🎤 Отключение микрофона/звука
|
- 🎤 Отключение микрофона/звука
|
||||||
- **Настройка:** `/log-setup moderation`
|
- **Настройка:** `!log-setup moderation` / `!log-setup levelup`
|
||||||
|
|
||||||
### 🎮 Развлекательные команды
|
### 🎮 Развлекательные команды
|
||||||
|
|
||||||
**Игры:**
|
**Игры:**
|
||||||
- `/coin` - подбросить монетку
|
- `!coin` — подбросить монетку
|
||||||
- `/dice` - бросить кубик
|
- `!dice` — бросить кубик
|
||||||
- `/8ball` - магический шар
|
- `!8ball` — магический шар
|
||||||
- `/ship @user1 @user2` - совместимость
|
- `!ship @user1 @user2` — совместимость
|
||||||
- `/rate` - оценить что-то
|
- `!rate` — оценить что-то
|
||||||
- `/gay/@fat/@iq/@pp` - процентомеры
|
- `!gay`, `!fat`, `!iq`, `!pp` — процентомеры
|
||||||
|
|
||||||
**Взаимодействия:**
|
**GIF-интерактив (nekos.life):**
|
||||||
- `/hug/@kiss/@pat/@poke` - обнять/поцеловать/погладить/ткнуть
|
- `!hug`, `!kiss`, `!poke`, `!slap`, `!tickle`, `!cuddle`, `!feed`
|
||||||
- `/slap/@tickle/@cuddle` - шлепнуть/пощекотать/обнять
|
|
||||||
- `/compliment/@roast` - комплимент/жарка
|
**Развлечения (текст):**
|
||||||
- `/feed` - покормить
|
- `!pat` — погладить (без GIF, несколько pat-pat)
|
||||||
|
- `!compliment`, `!roast` — комплимент / жарка
|
||||||
|
|
||||||
### 🛡️ Модерация
|
### 🛡️ Модерация
|
||||||
- `/ban/@kick` - бан/кик пользователя
|
- `!ban`, `!kick` — бан / кик пользователя
|
||||||
- `/clear <amount>` - очистить сообщения
|
- `!clear <amount>` — очистить сообщения
|
||||||
- Автологирование всех действий
|
- Автологирование всех действий
|
||||||
|
|
||||||
### 🎨 Кастомные роли
|
### 🎨 Кастомные роли
|
||||||
- `/createrole` - создать роль
|
- `!createrole` — создать роль
|
||||||
- `/deleterole` - удалить роль
|
- `!deleterole` — удалить роль
|
||||||
- `/editrole` - редактировать роль
|
- `!editrole` — редактировать роль
|
||||||
- `/myroles` - мои роли
|
- `!myroles` — мои роли
|
||||||
|
|
||||||
### 📈 Статистика
|
### 📈 Статистика
|
||||||
- `/stats [@user]` - статистика пользователя
|
- `!stats` — статистика сервера
|
||||||
- `/rep/@minusrep` - репутация
|
- `!rep`, `!minusrep` — репутация
|
||||||
- `/reptop` - топ по репутации
|
- `!reptop` — топ по репутации
|
||||||
- `/serverstats` - статистика сервера
|
|
||||||
|
|
||||||
### 🔧 Утилиты
|
### 🔧 Утилиты
|
||||||
- `/avatar [@user]` - аватар пользователя
|
- `!avatar [@user]` — аватар пользователя
|
||||||
- `/info [@user]` - информация о пользователе
|
- `!info [@user]` — информация о пользователе
|
||||||
- `/poll` - создать голосование
|
- `!poll` — создать голосование
|
||||||
- `/ping` - пинг бота
|
- `!ping` — пинг бота
|
||||||
- `/help` - справка
|
- `!help` — справка
|
||||||
- `/tts <text>` - озвучить текст
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -99,26 +98,72 @@
|
||||||
|
|
||||||
### Личные войс-каналы
|
### Личные войс-каналы
|
||||||
```
|
```
|
||||||
/voice-setup trigger <channel> - канал-триггер
|
!voice-setup trigger <channel> - канал-триггер
|
||||||
/voice-setup category <category> - категория для каналов
|
!voice-setup category <category> - категория для каналов
|
||||||
/voice-setup text-channel <channel> - текстовый канал для панелей
|
!voice-setup text-channel <channel> - текстовый канал для панелей
|
||||||
/voice-setup info - показать настройки
|
!voice-setup info - показать настройки
|
||||||
```
|
```
|
||||||
|
|
||||||
### Счетчик участников
|
### Счетчик участников
|
||||||
```
|
```
|
||||||
/counter-setup channel <channel> - установить канал
|
!counter-setup channel <channel> - установить канал
|
||||||
/counter-setup format <format> - формат ({count})
|
!counter-setup format <format> - формат ({count})
|
||||||
/counter-setup info - показать настройки
|
!counter-setup info - показать настройки
|
||||||
```
|
```
|
||||||
|
|
||||||
### Логирование
|
### Логирование
|
||||||
```
|
```
|
||||||
/log-setup moderation <channel> - канал логов модерации
|
!log-setup moderation <channel> - канал логов модерации
|
||||||
/log-setup levelup <channel> - канал уведомлений level up
|
!log-setup levelup <channel> - канал уведомлений level up
|
||||||
/log-setup info - показать настройки
|
!log-setup info - показать настройки
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Сброс уровней (одноразовая команда)
|
||||||
|
```
|
||||||
|
!resetlevels
|
||||||
|
```
|
||||||
|
**Описание:** Сбрасывает уровни и XP всех пользователей на сервере.
|
||||||
|
|
||||||
|
**Требования:**
|
||||||
|
- Права администратора
|
||||||
|
- Команда работает только один раз
|
||||||
|
- Требует подтверждения через кнопки
|
||||||
|
|
||||||
|
**Что сбрасывается:**
|
||||||
|
- Уровень → 0
|
||||||
|
- XP → 0
|
||||||
|
|
||||||
|
**Что сохраняется:**
|
||||||
|
- Время в голосовых каналах
|
||||||
|
- Количество сообщений
|
||||||
|
- Репутация
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Команды супер-админа
|
||||||
|
|
||||||
|
**Супер-админ ID:** `1414975197269987471`
|
||||||
|
|
||||||
|
### Генерация рейтингов
|
||||||
|
```
|
||||||
|
!generateratings
|
||||||
|
```
|
||||||
|
**Описание:** Генерирует все rating значения для всех участников сервера.
|
||||||
|
|
||||||
|
**Генерируемые рейтинги:**
|
||||||
|
- Ship (0-100%)
|
||||||
|
- Gay (0-100%)
|
||||||
|
- IQ (0-200)
|
||||||
|
- PP (0-30 см)
|
||||||
|
- Mustache (0-100, редко 1000-1500)
|
||||||
|
- Fat (0-100%)
|
||||||
|
- Boykisser (0-100%)
|
||||||
|
|
||||||
|
**Особенности:**
|
||||||
|
- Не перезаписывает существующие значения
|
||||||
|
- Пропускает ботов
|
||||||
|
- Генерирует для всех участников сервера
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🗄️ База данных
|
## 🗄️ База данных
|
||||||
|
|
@ -143,15 +188,15 @@ gradle run
|
||||||
|
|
||||||
2. **Настройка:**
|
2. **Настройка:**
|
||||||
```
|
```
|
||||||
/voice-setup trigger #создать-канал
|
!voice-setup trigger #создать-канал
|
||||||
/voice-setup category Личные каналы
|
!voice-setup category Личные каналы
|
||||||
/voice-setup text-channel #управление
|
!voice-setup text-channel #управление
|
||||||
|
|
||||||
/counter-setup channel #счетчик
|
!counter-setup channel #счетчик
|
||||||
/counter-setup format 【👥】Участники: {count}
|
!counter-setup format 【👥】Участники: {count}
|
||||||
|
|
||||||
/log-setup moderation #логи
|
!log-setup moderation #логи
|
||||||
/log-setup levelup #level-up
|
!log-setup levelup #level-up
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Готово!** Зайди в канал-триггер → создастся личный канал
|
3. **Готово!** Зайди в канал-триггер → создастся личный канал
|
||||||
|
|
|
||||||
61
README.md
61
README.md
|
|
@ -8,12 +8,61 @@
|
||||||
# Запуск
|
# Запуск
|
||||||
gradle run
|
gradle run
|
||||||
|
|
||||||
# Настройка через Discord
|
# Настройка через Discord (префикс по умолчанию «!»)
|
||||||
/voice-setup trigger #создать-канал
|
!voice-setup trigger #создать-канал
|
||||||
/counter-setup channel #счетчик
|
!counter-setup channel #счетчик
|
||||||
/log-setup moderation #логи
|
!log-setup moderation #логи
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🔧 Административные команды
|
||||||
|
|
||||||
|
### Сброс уровней (одноразовая команда)
|
||||||
|
```
|
||||||
|
!resetlevels
|
||||||
|
```
|
||||||
|
**Описание:** Сбрасывает уровни и XP всех пользователей на сервере. Сохраняет время в войсе и количество сообщений.
|
||||||
|
|
||||||
|
**Требования:**
|
||||||
|
- Права администратора
|
||||||
|
- Команда работает только один раз
|
||||||
|
- Требует подтверждения через кнопки
|
||||||
|
|
||||||
|
**Что сбрасывается:**
|
||||||
|
- Уровень → 0
|
||||||
|
- XP → 0
|
||||||
|
|
||||||
|
**Что сохраняется:**
|
||||||
|
- Время в голосовых каналах
|
||||||
|
- Количество сообщений
|
||||||
|
- Репутация
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Генерация рейтингов (супер-админ)
|
||||||
|
```
|
||||||
|
!generateratings
|
||||||
|
```
|
||||||
|
**Описание:** Генерирует все rating значения (ship, gay, iq, pp, mustache, fat, boykisser) для всех участников сервера.
|
||||||
|
|
||||||
|
**Требования:**
|
||||||
|
- Только для супер-админа (ID: `1414975197269987471`)
|
||||||
|
- Не перезаписывает существующие значения
|
||||||
|
- Сохраняет в SQLite базу данных
|
||||||
|
|
||||||
|
**Генерируемые рейтинги:**
|
||||||
|
- Ship (0-100%)
|
||||||
|
- Gay (0-100%)
|
||||||
|
- IQ (0-200)
|
||||||
|
- PP (0-30 см)
|
||||||
|
- Mustache (0-100, редко 1000-1500)
|
||||||
|
- Fat (0-100%)
|
||||||
|
- Boykisser (0-100%)
|
||||||
|
|
||||||
|
**Особенности:**
|
||||||
|
- Хранится в таблице `ratings` в SQLite
|
||||||
|
- Пропускает ботов
|
||||||
|
- Показывает статистику генерации
|
||||||
|
|
||||||
## 📚 Документация
|
## 📚 Документация
|
||||||
|
|
||||||
- **[FEATURES.md](FEATURES.md)** - Полный список функций
|
- **[FEATURES.md](FEATURES.md)** - Полный список функций
|
||||||
|
|
@ -41,6 +90,10 @@ gradle run
|
||||||
- Java 21+
|
- Java 21+
|
||||||
- Gradle 8.x
|
- Gradle 8.x
|
||||||
|
|
||||||
|
## ❤️ Health (HTTP)
|
||||||
|
|
||||||
|
В `.env` можно задать **`HEALTH_HTTP_PORT`** (0–65535, **0** = не поднимать сервер). После готовности JDA слушает **`GET /health`** и отдаёт JSON (uptime, счётчики команд, версия из манифеста или `dev`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Версия:** 2.0 | **Лицензия:** MIT
|
**Версия:** 2.0 | **Лицензия:** MIT
|
||||||
|
|
|
||||||
928
TODO.md
928
TODO.md
|
|
@ -1,746 +1,254 @@
|
||||||
# TODO - План реализации функций
|
# TODO — Loki Bot v2.1
|
||||||
|
|
||||||
## Правила разработки
|
|
||||||
|
|
||||||
### Архитектура и код:
|
|
||||||
- KISS, DRY, SOLID принципы
|
|
||||||
- Файлы максимум 150-200 строк, стараться делать 100 строк
|
|
||||||
- В одной папке не больше 6 файлов
|
|
||||||
- Не создавать новые MD файлы без необходимости
|
|
||||||
- Разделение по доменам, а не по типам артефактов
|
|
||||||
|
|
||||||
### Принципы структуры:
|
|
||||||
- Пакет = домен/фича, а не тип файла (не "команды/листенеры", а "profile/level/voice")
|
|
||||||
- Каждая фича содержит свои команды, сервисы, репозитории
|
|
||||||
- Единый слой персистентности через repository/
|
|
||||||
- Интеграции в отдельном пакете integration/
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Рефакторинг структуры проекта
|
|
||||||
|
|
||||||
### Целевая оценка: 7/10
|
|
||||||
|
|
||||||
**Проблемы:**
|
|
||||||
1. `util/` - мусорная корзина (CooldownManager, RateLimiter, NekosLifeApi)
|
|
||||||
2. Два механизма персистентности (data/ и database/) без абстракции
|
|
||||||
3. `command/impl/entertainment/` - 23 файла в одном пакете
|
|
||||||
4. `exception/` - избыточный пакет для 2 файлов
|
|
||||||
5. `embed/` - presentation layer оторван от domain
|
|
||||||
6. `listener/` - смешаны разные ответственности
|
|
||||||
7. Техническое разделение вместо доменного
|
|
||||||
8. **LevelSystem - God Class (5/10):**
|
|
||||||
- Нарушение SRP: хранение + кэширование + формулы + лидерборды + прогресс-бары
|
|
||||||
- Прямая зависимость от JsonDataStore (нет интерфейса Repository)
|
|
||||||
- O(n) на каждое сообщение (читает/пишет весь JSON)
|
|
||||||
- Кэш messageTimestamps не персистентный (теряется при рестарте)
|
|
||||||
- Рассогласование формул: комментарий говорит 200, код использует 100
|
|
||||||
- LevelUpResult и LevelProgress - вложенные классы вместо отдельных
|
|
||||||
- getAllStats() возвращает mutable map (можно сломать консистентность)
|
|
||||||
9. **UserStats - анемичная модель (6/10):**
|
|
||||||
- Сеттеры позволяют нарушить инварианты (setLevel(999) без XP)
|
|
||||||
- incrementMessages() скрыто меняет lastMessageTime (side effect)
|
|
||||||
- Поля lastMessageTime и lastVoiceJoin не используются системой
|
|
||||||
- Нет equals/hashCode для использования в коллекциях
|
|
||||||
|
|
||||||
### Целевая структура (по доменам):
|
|
||||||
|
|
||||||
```
|
|
||||||
src/main/java/creator/loki/
|
|
||||||
├── App.java # Точка входа (временно, потом в bootstrap/)
|
|
||||||
│
|
|
||||||
├── core/ # Ядро системы
|
|
||||||
│ ├── BotInitializer
|
|
||||||
│ ├── EventDispatcher
|
|
||||||
│ ├── registry/ # Регистрация команд
|
|
||||||
│ │ ├── CommandRegistry
|
|
||||||
│ │ ├── SlashCommandRegistry
|
|
||||||
│ │ ├── PrefixCommandRegistrar
|
|
||||||
│ │ └── SlashCommandRegistrar
|
|
||||||
│ ├── security/ # Безопасность и ограничения
|
|
||||||
│ │ ├── RateLimiter # (из util/)
|
|
||||||
│ │ └── CooldownManager # (из util/)
|
|
||||||
│ └── exception/ # Глобальная обработка ошибок
|
|
||||||
│ └── CommandExceptionHandler
|
|
||||||
│
|
|
||||||
├── command/ # Базовые интерфейсы команд
|
|
||||||
│ ├── Command.java
|
|
||||||
│ ├── CommandContext.java
|
|
||||||
│ ├── CommandException.java # (из exception/)
|
|
||||||
│ ├── SlashCommand.java
|
|
||||||
│ ├── moderation/ # Модерация (3 файла)
|
|
||||||
│ │ ├── BanCommand
|
|
||||||
│ │ ├── KickCommand
|
|
||||||
│ │ └── ClearCommand
|
|
||||||
│ ├── entertainment/ # Развлечения
|
|
||||||
│ │ ├── games/ # Игры (12 файлов)
|
|
||||||
│ │ └── social/ # Взаимодействия (11 файлов)
|
|
||||||
│ ├── utility/ # Утилиты (5 файлов)
|
|
||||||
│ │ ├── HelpCommand
|
|
||||||
│ │ ├── PingCommand
|
|
||||||
│ │ ├── InfoCommand
|
|
||||||
│ │ ├── AvatarCommand
|
|
||||||
│ │ └── PollCommand
|
|
||||||
│ └── system/ # Системные команды
|
|
||||||
│ └── MetricsCommand # (из utility/)
|
|
||||||
│
|
|
||||||
├── feature/ # Крупные фичи/домены
|
|
||||||
│ ├── level/ # Система уровней
|
|
||||||
│ │ ├── LevelSystem
|
|
||||||
│ │ ├── UserStats
|
|
||||||
│ │ ├── LevelUtils # (из profile/)
|
|
||||||
│ │ ├── RankCommand # (из command/stats/)
|
|
||||||
│ │ ├── StatsCommand # (из command/stats/)
|
|
||||||
│ │ ├── LevelUpNotifier # (из listener/)
|
|
||||||
│ │ └── VoiceTracker # (из listener/)
|
|
||||||
│ │
|
|
||||||
│ ├── profile/ # Профили пользователей
|
|
||||||
│ │ ├── ProfileService
|
|
||||||
│ │ ├── ProfileGenerator
|
|
||||||
│ │ ├── ProfileData
|
|
||||||
│ │ ├── ProfileCommand # (из command/utility/)
|
|
||||||
│ │ └── ProfileEmbedFactory # (из embed/)
|
|
||||||
│ │
|
|
||||||
│ ├── reputation/ # Система репутации
|
|
||||||
│ │ ├── ReputationService # (логика из RepCommand)
|
|
||||||
│ │ ├── RepCommand # (из command/stats/)
|
|
||||||
│ │ ├── MinusRepCommand # (из command/stats/)
|
|
||||||
│ │ └── RepTopCommand # (из command/stats/)
|
|
||||||
│ │
|
|
||||||
│ ├── leaderboard/ # Таблица лидеров
|
|
||||||
│ │ ├── LeaderboardCommand # (NewLeaderboardCommand)
|
|
||||||
│ │ ├── LeaderboardService # (логика из команды)
|
|
||||||
│ │ ├── LeaderboardFormatter
|
|
||||||
│ │ ├── LeaderboardButtons
|
|
||||||
│ │ └── LeaderboardButtonListener
|
|
||||||
│ │
|
|
||||||
│ ├── voice/ # Личные голосовые каналы
|
|
||||||
│ │ ├── VoiceChannelManager
|
|
||||||
│ │ ├── VoiceChannelData
|
|
||||||
│ │ ├── VoicePreset
|
|
||||||
│ │ ├── VoiceSetupCommand # (из command/setup/)
|
|
||||||
│ │ ├── VoiceChannelListener # (из listener/)
|
|
||||||
│ │ └── VoiceButtonHandler # (из listener/)
|
|
||||||
│ │
|
|
||||||
│ ├── counter/ # Счетчик участников
|
|
||||||
│ │ ├── CounterManager
|
|
||||||
│ │ ├── DailyStats
|
|
||||||
│ │ ├── CounterSetupCommand # (из command/setup/)
|
|
||||||
│ │ └── MemberCounterListener # (из listener/)
|
|
||||||
│ │
|
|
||||||
│ └── role/ # Кастомные роли
|
|
||||||
│ ├── CustomRoleManager
|
|
||||||
│ ├── CreateRoleCommand # (из command/roles/)
|
|
||||||
│ ├── DeleteRoleCommand # (из command/roles/)
|
|
||||||
│ ├── EditRoleCommand # (из command/roles/)
|
|
||||||
│ ├── MyRolesCommand # (из command/roles/)
|
|
||||||
│ └── RoleCommandValidator # (из command/roles/)
|
|
||||||
│
|
|
||||||
├── event/ # Обработчики событий Discord
|
|
||||||
│ ├── ReadyListener # (из listener/)
|
|
||||||
│ ├── MessageListener # (из listener/)
|
|
||||||
│ ├── SlashCommandListener # (из listener/)
|
|
||||||
│ ├── HelpMenuHandler # (из listener/)
|
|
||||||
│ └── ModerationLogger # (из listener/)
|
|
||||||
│
|
|
||||||
├── repository/ # Единый слой данных
|
|
||||||
│ ├── UserRepository # Интерфейс
|
|
||||||
│ ├── GuildConfigRepository # Интерфейс
|
|
||||||
│ └── impl/
|
|
||||||
│ ├── JsonUserRepository # (из data/)
|
|
||||||
│ └── SqliteConfigRepository # (из database/)
|
|
||||||
│
|
|
||||||
├── database/ # Инфраструктура БД
|
|
||||||
│ ├── Database
|
|
||||||
│ ├── DatabaseSchema
|
|
||||||
│ └── DatabaseMigration
|
|
||||||
│
|
|
||||||
├── integration/ # Внешние API
|
|
||||||
│ └── nekos/
|
|
||||||
│ └── NekosLifeApi # (из util/)
|
|
||||||
│
|
|
||||||
├── config/ # Конфигурация
|
|
||||||
│ ├── BotConfig
|
|
||||||
│ └── ConfigLoader
|
|
||||||
│
|
|
||||||
├── embed/ # Общие шаблоны embed
|
|
||||||
│ ├── EmbedFactory
|
|
||||||
│ └── EmbedTemplates
|
|
||||||
│
|
|
||||||
├── metrics/ # Метрики
|
|
||||||
│ └── MetricsCollector
|
|
||||||
│
|
|
||||||
└── di/ # Dependency Injection
|
|
||||||
└── BotModule
|
|
||||||
```
|
|
||||||
|
|
||||||
### План действий (этапы):
|
|
||||||
|
|
||||||
#### Этап 1: Фундамент (делаем первым)
|
|
||||||
|
|
||||||
**1. Создать repository/ (средний → высокий приоритет)**
|
|
||||||
- [x] Создать `repository/`
|
|
||||||
- [x] Создать интерфейс `repository/UserStatsRepository.java`
|
|
||||||
- [x] Создать `repository/impl/`
|
|
||||||
- [x] Создать `repository/impl/JsonUserStatsRepository.java` (обёртка над JsonDataStore)
|
|
||||||
- [ ] Создать интерфейс `repository/GuildConfigRepository.java`
|
|
||||||
- [ ] Рефакторить `database/ConfigRepository` → использовать интерфейс
|
|
||||||
|
|
||||||
**2. Реорганизовать util/ (мусорная корзина)**
|
|
||||||
- [x] Создать `core/security/`
|
|
||||||
- [x] Переместить `util/CooldownManager` → `core/security/CooldownManager`
|
|
||||||
- [x] Переместить `util/GlobalRateLimiter` → `core/security/RateLimiter`
|
|
||||||
- [x] Создать `integration/nekos/`
|
|
||||||
- [x] Переместить `util/NekosLifeApi` → `integration/nekos/NekosLifeApi`
|
|
||||||
- [x] Удалить пустую папку `util/`
|
|
||||||
- [x] Обновить все импорты в командах
|
|
||||||
- [x] Добавить `UserStatsRepository` в `BotModule`
|
|
||||||
|
|
||||||
**3. Удалить импорты AudioManager и мертвые ссылки**
|
|
||||||
- [x] Удалить `import creator.loki.audio.AudioManager` из `BotInitializer`
|
|
||||||
- [x] Удалить `import creator.loki.audio.AudioManager` из `PrefixCommandRegistrar`
|
|
||||||
- [x] Удалить `import creator.loki.audio.AudioManager` из `SlashCommandRegistrar`
|
|
||||||
- [x] Удалить параметр `AudioManager audioManager` из конструкторов
|
|
||||||
- [x] Удалить поля `private final AudioManager audioManager`
|
|
||||||
- [x] Удалить `registry.register(new TtsCommand(...))` из `PrefixCommandRegistrar`
|
|
||||||
- [x] Удалить `registry.register(new TtsSlashCommand(...))` из `SlashCommandRegistrar`
|
|
||||||
- [x] Исправить `registry.register(new NewLeaderboardCommand(...))` → использовать новый путь
|
|
||||||
- [x] Удалить все `registry.register(new *SlashCommand(...))` из `SlashCommandRegistrar`
|
|
||||||
- [x] Удалить `import creator.loki.command.slash.*` из `SlashCommandRegistrar`
|
|
||||||
- [x] Проверить компиляцию: `gradle build`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Этап 2: feature/level/ (зависит от Этапа 1)
|
|
||||||
|
|
||||||
**4. Создать feature/level/domain/**
|
|
||||||
- [x] Создать `feature/level/`
|
|
||||||
- [x] Создать `feature/level/domain/`
|
|
||||||
- [x] Создать `feature/level/domain/XpFormula.java`
|
|
||||||
- [x] Вынести `LevelUpResult` из `LevelSystem` → `feature/level/domain/LevelUpResult.java`
|
|
||||||
- [x] Вынести `LevelProgress` из `LevelSystem` → `feature/level/domain/LevelProgress.java`
|
|
||||||
- [x] Переместить `level/UserStats` → `feature/level/domain/UserStats.java`
|
|
||||||
- [x] **НЕ делать immutable сейчас** (Gson требует сеттеры, сделаем после Repository)
|
|
||||||
- [x] Добавить `equals()` и `hashCode()` в `UserStats`
|
|
||||||
- [x] Переименовать `incrementMessages()` → `recordMessage(long timestamp)`
|
|
||||||
|
|
||||||
**5. Создать feature/level/service/**
|
|
||||||
- [x] Создать `feature/level/service/`
|
|
||||||
- [x] Создать `feature/level/service/MessageCooldownService.java`
|
|
||||||
- [x] Создать `feature/level/service/LeaderboardService.java`
|
|
||||||
- [x] Рефакторить `level/LevelSystem` → `feature/level/service/LevelService.java`
|
|
||||||
- [x] Использовать `UserStatsRepository` вместо `JsonDataStore`
|
|
||||||
- [x] Использовать `XpFormula` для вычислений
|
|
||||||
- [x] Использовать `MessageCooldownService` для кулдаунов
|
|
||||||
- [x] Убрать методы лидерборда (в `LeaderboardService`)
|
|
||||||
- [x] Обновить все импорты UserStats на новый путь
|
|
||||||
- [x] Удалить старый `level/UserStats.java`
|
|
||||||
- [x] Добавить сервисы в `BotModule`
|
|
||||||
|
|
||||||
**6. Переместить команды и листенеры в feature/level/**
|
|
||||||
- [x] Переместить `command/impl/stats/RankCommand` → `feature/level/RankCommand`
|
|
||||||
- [x] Переместить `command/impl/stats/StatsCommand` → `feature/level/StatsCommand`
|
|
||||||
- [x] Переместить `listener/LevelUpNotifier` → `feature/level/LevelUpNotifier`
|
|
||||||
- [x] Переместить `listener/VoiceTracker` → `feature/level/VoiceTracker`
|
|
||||||
- [x] Обновить все импорты
|
|
||||||
- [x] Обновить `BotInitializer`, `BotModule`, `PrefixCommandRegistrar`
|
|
||||||
- [x] Обновить команды для использования `LevelService` вместо `LevelSystem`
|
|
||||||
|
|
||||||
**7. Создать команду сброса уровней (миграция на новую систему)**
|
|
||||||
- [ ] Создать `feature/level/ResetLevelsCommand.java`:
|
|
||||||
- Только для администраторов
|
|
||||||
- Подтверждение через кнопки (Да/Нет)
|
|
||||||
- Сброс всех уровней и XP
|
|
||||||
- Очистка таблицы `user_stats` в БД
|
|
||||||
- Логирование действия
|
|
||||||
- Уведомление в канал логов
|
|
||||||
- Embed с предупреждением о необратимости
|
|
||||||
- [ ] Зарегистрировать команду в `PrefixCommandRegistrar`
|
|
||||||
- [ ] Добавить в категорию "Настройка" (только админы)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Этап 3: Остальные feature/ (параллельно, независимо)
|
|
||||||
|
|
||||||
**7. Создать feature/voice/ (голосовые каналы)**
|
|
||||||
- [ ] Создать `feature/voice/`
|
|
||||||
- [ ] Переместить `voice/VoiceChannelManager` → `feature/voice/`
|
|
||||||
- [ ] Переместить `voice/VoiceChannelData` → `feature/voice/`
|
|
||||||
- [ ] Переместить `voice/VoicePreset` → `feature/voice/`
|
|
||||||
- [ ] Переместить `command/impl/setup/VoiceSetupCommand` → `feature/voice/`
|
|
||||||
- [ ] Переместить `listener/VoiceChannelListener` → `feature/voice/`
|
|
||||||
- [ ] Переместить `listener/VoiceButtonHandler` → `feature/voice/`
|
|
||||||
- [ ] Удалить пустую папку `voice/`
|
|
||||||
- [ ] Обновить импорты
|
|
||||||
|
|
||||||
**8. Создать feature/counter/ (счетчик участников)**
|
|
||||||
- [ ] Создать `feature/counter/`
|
|
||||||
- [ ] Переместить `counter/CounterManager` → `feature/counter/`
|
|
||||||
- [ ] Переместить `counter/DailyStats` → `feature/counter/`
|
|
||||||
- [ ] Переместить `command/impl/setup/CounterSetupCommand` → `feature/counter/`
|
|
||||||
- [ ] Переместить `listener/MemberCounterListener` → `feature/counter/`
|
|
||||||
- [ ] Удалить пустую папку `counter/`
|
|
||||||
- [ ] Обновить импорты
|
|
||||||
|
|
||||||
**9. Создать feature/role/ (кастомные роли)**
|
|
||||||
- [ ] Создать `feature/role/`
|
|
||||||
- [ ] Переместить `role/CustomRoleManager` → `feature/role/`
|
|
||||||
- [ ] Переместить `command/impl/roles/CreateRoleCommand` → `feature/role/`
|
|
||||||
- [ ] Переместить `command/impl/roles/DeleteRoleCommand` → `feature/role/`
|
|
||||||
- [ ] Переместить `command/impl/roles/EditRoleCommand` → `feature/role/`
|
|
||||||
- [ ] Переместить `command/impl/roles/MyRolesCommand` → `feature/role/`
|
|
||||||
- [ ] Переместить `command/impl/roles/RoleCommandValidator` → `feature/role/`
|
|
||||||
- [ ] Удалить пустые папки `role/` и `command/impl/roles/`
|
|
||||||
- [ ] Обновить импорты
|
|
||||||
|
|
||||||
**10. Создать feature/reputation/ (система репутации)**
|
|
||||||
- [ ] Создать `feature/reputation/`
|
|
||||||
- [ ] Создать `feature/reputation/ReputationService.java` (вынести логику из команд)
|
|
||||||
- [ ] Переместить `command/impl/stats/RepCommand` → `feature/reputation/`
|
|
||||||
- [ ] Переместить `command/impl/stats/MinusRepCommand` → `feature/reputation/`
|
|
||||||
- [ ] Переместить `command/impl/stats/RepTopCommand` → `feature/reputation/`
|
|
||||||
- [ ] Рефакторить команды: использовать `ReputationService`
|
|
||||||
- [ ] Обновить импорты
|
|
||||||
|
|
||||||
**11. Создать feature/leaderboard/ (таблица лидеров)**
|
|
||||||
- [ ] Создать `feature/leaderboard/`
|
|
||||||
- [ ] Переместить `command/impl/stats/leaderboard/NewLeaderboardCommand` → `feature/leaderboard/LeaderboardCommand`
|
|
||||||
- [ ] Переместить `command/impl/stats/leaderboard/LeaderboardFormatter` → `feature/leaderboard/`
|
|
||||||
- [ ] Переместить `command/impl/stats/leaderboard/LeaderboardButtons` → `feature/leaderboard/`
|
|
||||||
- [ ] Переместить `command/impl/stats/leaderboard/LeaderboardButtonListener` → `feature/leaderboard/`
|
|
||||||
- [ ] Создать `feature/leaderboard/LeaderboardService.java` (использовать из level/)
|
|
||||||
- [ ] Удалить пустую папку `command/impl/stats/`
|
|
||||||
- [ ] Обновить импорты
|
|
||||||
|
|
||||||
**12. Создать feature/profile/ (профили пользователей)**
|
|
||||||
- [ ] Создать `feature/profile/`
|
|
||||||
- [ ] Переместить `profile/ProfileService` → `feature/profile/`
|
|
||||||
- [ ] Переместить `profile/ProfileGenerator` → `feature/profile/`
|
|
||||||
- [ ] Переместить `profile/ProfileData` → `feature/profile/`
|
|
||||||
- [ ] Переместить `command/impl/utility/ProfileCommand` → `feature/profile/`
|
|
||||||
- [ ] Создать `feature/profile/ProfileEmbedFactory.java` (вынести из команды)
|
|
||||||
- [ ] Удалить пустую папку `profile/`
|
|
||||||
- [ ] Обновить импорты
|
|
||||||
|
|
||||||
**13. Создать feature/metrics/ (метрики системы)**
|
|
||||||
- [ ] Создать `feature/metrics/`
|
|
||||||
- [ ] Переместить `metrics/MetricsCollector` → `feature/metrics/`
|
|
||||||
- [ ] Переместить `command/impl/utility/MetricsCommand` → `feature/metrics/`
|
|
||||||
- [ ] Создать `feature/metrics/MetricsService.java` (если нужна логика)
|
|
||||||
- [ ] Удалить пустую папку `metrics/`
|
|
||||||
- [ ] Обновить импорты
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Этап 4: Инфраструктура (после feature/)
|
|
||||||
|
|
||||||
**14. Реорганизовать event/ (listener)**
|
|
||||||
- [ ] Создать `event/`
|
|
||||||
- [ ] Создать `event/gateway/` (JDA Gateway events)
|
|
||||||
- [ ] Переместить `listener/ReadyListener` → `event/gateway/`
|
|
||||||
- [ ] Переместить `listener/MessageListener` → `event/gateway/`
|
|
||||||
- [ ] Создать `event/gateway/GuildListener.java` (member join/leave + moderation)
|
|
||||||
- [ ] Переместить `listener/ModerationLogger` → `event/gateway/GuildListener` (или отдельно)
|
|
||||||
- [ ] Создать `event/interaction/` (Button/Menu interactions)
|
|
||||||
- [ ] Переместить `listener/SlashCommandListener` → `event/interaction/`
|
|
||||||
- [ ] Переместить `listener/HelpMenuHandler` → `event/interaction/`
|
|
||||||
- [ ] Удалить пустую папку `listener/`
|
|
||||||
- [ ] Обновить импорты
|
|
||||||
|
|
||||||
**15. Реорганизовать core/**
|
|
||||||
- [ ] Создать `core/registry/`
|
|
||||||
- [ ] Переместить `core/CommandRegistry` → `core/registry/`
|
|
||||||
- [ ] Переместить `core/SlashCommandRegistry` → `core/registry/`
|
|
||||||
- [ ] Переместить `core/PrefixCommandRegistrar` → `core/registry/`
|
|
||||||
- [ ] Переместить `core/SlashCommandRegistrar` → `core/registry/`
|
|
||||||
- [ ] Создать `core/exception/`
|
|
||||||
- [ ] Переместить `exception/CommandExceptionHandler` → `core/exception/`
|
|
||||||
- [ ] Переместить `exception/CommandException` → `command/CommandException`
|
|
||||||
- [ ] Удалить пустую папку `exception/`
|
|
||||||
- [ ] Обновить импорты
|
|
||||||
|
|
||||||
**16. Почистить command/**
|
|
||||||
- [ ] Переименовать `command/impl/entertainment/interaction/` → `command/entertainment/social/`
|
|
||||||
- [ ] Удалить `command/impl/setup/` (команды перенесены в feature/)
|
|
||||||
- [ ] Удалить `command/impl/utility/SwapCommand.java` (не используется)
|
|
||||||
- [ ] Оставить в `command/` только:
|
|
||||||
- Интерфейсы: `Command.java`, `CommandContext.java`, `SlashCommand.java`, `CommandException.java`
|
|
||||||
- `command/moderation/` (если не выносим в feature/)
|
|
||||||
- `command/entertainment/` (кросс-доменные развлечения)
|
|
||||||
- `command/utility/` (HelpCommand, PingCommand, InfoCommand, AvatarCommand, PollCommand)
|
|
||||||
|
|
||||||
**17. Вынести App.java из корня**
|
|
||||||
- [ ] Создать `bootstrap/`
|
|
||||||
- [ ] Создать `bootstrap/App.java`
|
|
||||||
- [ ] Переместить логику инициализации из корневого `App.java`
|
|
||||||
- [ ] Удалить корневой `App.java`
|
|
||||||
- [ ] Обновить `build.gradle` (mainClass)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Этап 5: Финальная полировка (опционально)
|
|
||||||
|
|
||||||
**18. UserStats immutable (после Repository)**
|
|
||||||
- [ ] Создать `UserStats.Builder`
|
|
||||||
- [ ] Убрать public сеттеры (оставить package-private для Gson)
|
|
||||||
- [ ] Добавить методы `addXP()`, `recordMessage()`, `addVoiceTime()`
|
|
||||||
- [ ] Добавить `getLevel(XpFormula)` вместо внешних вычислений
|
|
||||||
- [ ] Обновить `JsonUserStatsRepository` для работы с immutable
|
|
||||||
|
|
||||||
**19. Обновить документацию**
|
|
||||||
- [ ] Обновить `FEATURES.md` (убрать TTS, добавить новые фичи)
|
|
||||||
- [ ] Обновить `README.md` (новая структура проекта)
|
|
||||||
- [ ] Создать `ARCHITECTURE.md` (описание доменов и feature/)
|
|
||||||
- [ ] Обновить комментарии в коде
|
|
||||||
|
|
||||||
**20. Финальная проверка**
|
|
||||||
- [ ] `gradle build` - компиляция без ошибок
|
|
||||||
- [ ] Проверить все импорты
|
|
||||||
- [ ] Проверить что нет папок с >6 файлами
|
|
||||||
- [ ] Проверить что нет файлов >200 строк
|
|
||||||
- [ ] Запустить бота и протестировать основные команды
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Рефакторинг кода (Code Quality)
|
|
||||||
|
|
||||||
### Убрать if-else леса в rating командах
|
|
||||||
|
|
||||||
**Проблема:**
|
|
||||||
Все rating команды (MustacheCommand, IqCommand, FatCommand, GayCommand, PpCommand, BoykisserCommand) содержат огромные if-else цепочки для определения уровня/комментария/цвета.
|
|
||||||
|
|
||||||
**Пример плохого кода (MustacheCommand):**
|
|
||||||
```java
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Решение:**
|
|
||||||
Создать общий класс `RatingLevel` с конфигурацией уровней:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class RatingLevel {
|
|
||||||
private final int threshold;
|
|
||||||
private final String comment;
|
|
||||||
private final Color color;
|
|
||||||
|
|
||||||
public static RatingLevel findLevel(int value, List<RatingLevel> levels) {
|
|
||||||
return levels.stream()
|
|
||||||
.filter(level -> value >= level.threshold)
|
|
||||||
.findFirst()
|
|
||||||
.orElse(levels.get(levels.size() - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**План действий:**
|
|
||||||
- [x] Создать `command/impl/entertainment/games/rating/RatingLevel.java`
|
|
||||||
- [x] Создать `command/impl/entertainment/games/rating/RatingConfig.java` с предустановленными конфигурациями
|
|
||||||
- [x] Рефакторить MustacheCommand - использовать RatingLevel
|
|
||||||
- [x] Рефакторить IqCommand - использовать RatingLevel
|
|
||||||
- [x] Рефакторить FatCommand - использовать RatingLevel
|
|
||||||
- [x] Рефакторить GayCommand - использовать RatingLevel (без уровней, только прогресс-бар)
|
|
||||||
- [x] Рефакторить PpCommand - использовать RatingLevel
|
|
||||||
- [x] Рефакторить BoykisserCommand - использовать RatingLevel
|
|
||||||
- [x] Проверить что все команды работают корректно
|
|
||||||
- [x] Удалить дублирующийся код
|
|
||||||
|
|
||||||
**Приоритет:** ✅ **ГОТОВО**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Основные задачи
|
|
||||||
|
|
||||||
### 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 за минуту в войсе
|
| C1–C15 | ✅ | Предыдущие раунды | См. git history |
|
||||||
- Обновление каждую минуту вместо 5 минут
|
| J21 | ✅ | Java 17 → 21 | `build.gradle`: source/target 21 |
|
||||||
- Метод `addVoiceTime` теперь возвращает `LevelUpResult` для уведомлений
|
| A05 | ✅ | EventDispatcher: CachedThreadPool → VirtualThreadPool | `Executors.newVirtualThreadPerTaskExecutor()` |
|
||||||
|
| A06 | ✅ | SwapCommand + RepTopCommand: `new Thread()` → `Thread.startVirtualThread()` | |
|
||||||
### ✅ Система личных войс-каналов (базовая)
|
| A25 | ✅ | CommandExceptionHandler: instanceof pattern matching | |
|
||||||
- `VoiceChannelManager` - управление каналами
|
| A26 | ✅ | ClearCommand: instanceof pattern matching | |
|
||||||
- `VoiceChannelListener` - автоматическое создание/удаление
|
| A27 | ✅ | DatabaseMigration: instanceof pattern matching | |
|
||||||
- `VoiceChannelData` и `VoicePreset` - модели данных
|
| A28 | ✅ | InfoCommand: switch → arrow-switch | |
|
||||||
- Автоматическое создание при входе в триггер-канал
|
| A29 | ✅ | RateCommand: `"⭐".repeat(rating)` + `"☆".repeat()` | |
|
||||||
- Автоматическое удаление пустых каналов
|
| A32 | ✅ | .collect(Collectors.toList()) → .toList() | SwapCommand, RepTopCommand, ChoiceOptionParser |
|
||||||
- Панель управления с кнопками (требуется обработчик кнопок)
|
| A33 | ✅ | HelpCategories: Map.ofEntries() вместо HashMap + unmodifiable | |
|
||||||
|
| A02 | ✅ | RankCardGenerator: блокировка Event Thread | Исправлено (команды на virtual threads) |
|
||||||
### ✅ Счетчик участников
|
| A10 | ✅ | RankCardGenerator: утечка InputStream | try-with-resources в loadFont + loadBackground |
|
||||||
- `CounterManager` - управление счетчиком
|
| A11 | ✅ | RankCardGenerator: шрифты не кэшировались | ConcurrentHashMap cache |
|
||||||
- `MemberCounterListener` - отслеживание входов/выходов
|
| A12 | ✅ | RankCardGenerator: SVG-иконки не работали | Заменены на геометрические фигуры (Java2D) |
|
||||||
- `DailyStats` - дневная статистика
|
| A23 | ✅ | RankCardGenerator: throws Exception | loadFont безопасный |
|
||||||
- Обновление каждые 5 минут
|
| A03 | ✅ | Database: race condition на connection | synchronized методы |
|
||||||
- История за 30 дней
|
| A13 | ✅ | NekosLifeApi: Thread.sleep в catch | Сон вынесен в retry loop |
|
||||||
|
| A14 | ✅ | GenerateRatingsCommand: блокировка event-потока | `complete()` вместо `queue()` |
|
||||||
### ✅ SQLite база данных + автомиграция
|
| A35 | ✅ | GenerateRatingsCommand: опечатка | `Пропускаемботов` → `Пропускаем ботов` |
|
||||||
- `Database` - менеджер SQLite
|
| A04 | ✅ | PrefixCommandRegistrar: God Constructor (22 param) | Разбит на 4 per-feature CommandRegistrar, Multibinder, 1 param |
|
||||||
- `DatabaseMigration` - автоматическая миграция из JSON
|
| A21 | ✅ | BotModule: путь к БД хардкодом | Вынесен в .env (`DB_PATH`) + BotConfig |
|
||||||
- `ConfigRepository` - работа с конфигурацией
|
| A34 | ✅ | CommandCategories: мёртвый код | Уже удалён (старый пакет) |
|
||||||
- Все таблицы созданы автоматически
|
| A37 | ✅ | ProfileCommand: регистрация в PrefixCommandRegistrar | LevelCommandRegistrar |
|
||||||
- Миграция данных при первом запуске
|
|
||||||
|
|
||||||
### ✅ Команды настройки
|
|
||||||
- `/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
|
- KISS, DRY, SOLID
|
||||||
- ✅ Красивые embed сообщения при повышении уровня
|
- Файлы ≤150–200 строк (в идеале ≤100)
|
||||||
- ✅ Отправка в тот же канал где получен XP
|
- В папке ≤6 файлов
|
||||||
- ✅ Для войса - отправка в текстовый канал связанный с войсом
|
- Пакет = домен/фича, а не тип файла (`feature/level/`, а не `commands/listeners/`)
|
||||||
- ✅ Показывать прогресс до следующего уровня
|
- Каждая фича содержит свои команды, сервисы, репозитории
|
||||||
|
- Единый слой персистентности через `repository/`
|
||||||
**Статус:** 🔴 Не начато
|
- Интеграции в `integration/`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. 🛠️ Команды для настройки систем (Admin Commands)
|
## 🔴 Проблемы (результаты аудита, которые ещё не исправлены)
|
||||||
|
|
||||||
#### Voice System Setup:
|
### HIGH
|
||||||
- `/voice-setup trigger <channel>` - установить канал-триггер
|
|
||||||
- `/voice-setup category <category>` - установить категорию для личных каналов
|
|
||||||
- `/voice-setup text-channel <channel>` - установить текстовый канал для уведомлений
|
|
||||||
|
|
||||||
#### Counter Setup:
|
| # | Приоритет | Проблема | Статус |
|
||||||
- `/counter-setup channel <voice-channel>` - установить канал для счетчика
|
|---|-----------|----------|--------|
|
||||||
- `/counter-setup format <format>` - изменить формат отображения
|
| A07 | 🟠 **HIGH** | `HealthHttpServer` — single thread executor | ✅ Virtual threads |
|
||||||
- `/counter stats` - показать статистику за период
|
| A08 | 🟠 **HIGH** | Тестовое покрытие ~5% (~34 теста на 149 продакшен-файлов). Нужно 20-30% | ⏳ 268 тестов в 22 файлах |
|
||||||
|
| A09 | 🟠 **HIGH** | Нет Docker / docker-compose | ✅ Dockerfile + docker-compose + .env.example |
|
||||||
|
| A01 | ⚪ **INFO** | `.env` с токеном в git-истории (локальный репозиторий) | ➖ |
|
||||||
|
|
||||||
**Статус:** 🔴 Не начато
|
### MEDIUM
|
||||||
|
|
||||||
|
| # | Приоритет | Проблема | Статус |
|
||||||
|
|---|-----------|----------|--------|
|
||||||
|
| A15 | 🟡 **MEDIUM** | ~33 файла команд в `command/impl/` — не перенесены в `feature/*/commands/` | ✅ 33 файла перенесены |
|
||||||
|
| A16 | 🟡 **MEDIUM** | ~16 репозиториев в `database/` — не разнесены в `feature/*/repository/` | ✅ 13+3 репозитория перенесены |
|
||||||
|
| A18 | 🟡 **MEDIUM** | `DatabaseMigration` — устаревший код для JSON→SQLite. Нужно оценить | ✅ Harmless, idempotent — keep as-is |
|
||||||
|
| A19 | 🟡 **MEDIUM** | Нет статического анализа (checkstyle / spotbugs) | ✅ checkstyle.xml + spotbugs + 169 known violations |
|
||||||
|
| A20 | 🟡 **MEDIUM** | Нет версионного каталога (`libs.versions.toml`) | ✅ gradle/libs.versions.toml, build.gradle переписан |
|
||||||
|
| A22 | 🟡 **MEDIUM** | Нет пула соединений (HikariCP). Голый JDBC | ✅ HikariCP 5.1.0 + proxy-based prepare() |
|
||||||
|
| A24 | 🟡 **MEDIUM** | VoiceTracker, RateLimiter, CooldownManager, MuteScheduler — ScheduledExecutorService. virtual threads? | ✅ Virtual threads |
|
||||||
|
| A40 | 🟡 **MEDIUM** | `HelpMenuHandler` в `event/interaction/` — должен быть в `feature/help/` | ✅ Перенесён |
|
||||||
|
|
||||||
|
### LOW
|
||||||
|
|
||||||
|
| # | Приоритет | Проблема | Статус |
|
||||||
|
|---|-----------|----------|--------|
|
||||||
|
| A30 | 🟢 **LOW** | SlowmodeCommand: ручная проверка границ → Math.clamp (другая семантика) | ➖ |
|
||||||
|
| A31 | 🟢 **LOW** | ClearCommand: ручная проверка границ → Math.clamp (другая семантика) | ➖ |
|
||||||
|
| A39 | 🟢 **LOW** | EmbedFactory: `set*` вместо `with*` для builder | ✅ Renamed in 43 files |
|
||||||
|
| A36 | 🟢 **LOW** | RankCardGenerator: URI → URL, deprecated API | ✅ `URI.toURL()` → `new URL()` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📝 Приоритеты реализации
|
## 📋 План рефакторинга
|
||||||
|
|
||||||
### Высокий приоритет (сделать в первую очередь):
|
### Этап 1: Java 21 ✅ (14 items)
|
||||||
1. ⚡ Переделать систему опыта (простая формула)
|
- [x] EventDispatcher: VirtualThreadPool
|
||||||
2. 🎤 Базовая система личных войс-каналов (создание/удаление)
|
- [x] instanceof pattern matching (3 файла)
|
||||||
3. 📊 Счетчик участников
|
- [x] arrow-switch (InfoCommand)
|
||||||
|
- [x] String.repeat() (RateCommand)
|
||||||
|
- [x] .toList() (3 файла)
|
||||||
|
- [x] Map.ofEntries() (HelpCategories)
|
||||||
|
|
||||||
### Средний приоритет:
|
### Этап 2: Рефакторинг PrefixCommandRegistrar ✅
|
||||||
4. 🎤 Настройки войс-каналов через кнопки
|
- [x] Создан интерфейс `CommandRegistrar`
|
||||||
5. 🎨 Улучшение уведомлений о level up
|
- [x] 4 per-feature регистратора
|
||||||
6. 🎤 Система пресетов для войс-каналов
|
- [x] Guice Multibinder для сбора регистраторов
|
||||||
|
- [x] 22 параметра → 1 (`Set<CommandRegistrar>`)
|
||||||
|
|
||||||
### Низкий приоритет:
|
### Этап 3: Critical-баги ✅
|
||||||
7. 🛠️ Админ команды для настройки
|
- [x] RankCardGenerator: async + try-with-resources + cache + SVG
|
||||||
|
- [x] Database: synchronized connection
|
||||||
|
- [x] NekosLifeApi: чистый retry loop
|
||||||
|
- [x] GenerateRatingsCommand: async через complete()
|
||||||
|
- [x] DB path: вынесен в .env
|
||||||
|
|
||||||
|
### Этап 4: Перемещение файлов в feature-пакеты ✅
|
||||||
|
- [x] `command/impl/utility/` → `feature/*/commands/`
|
||||||
|
- [x] `command/impl/entertainment/` → `feature/*/commands/`
|
||||||
|
- [x] `command/entertainment/social/` → `feature/social/commands/`
|
||||||
|
- [x] `database/*/` → `feature/*/repository/`
|
||||||
|
- [x] `event/interaction/HelpMenuHandler` → `feature/help/`
|
||||||
|
|
||||||
|
### Этап 5: Тесты (цель: 20-30% покрытие)
|
||||||
|
- [x] Тесты на репозитории (in-memory SQLite) — Config, ShipPair, UserStats, Ratings, Warnings, ActiveMutes
|
||||||
|
- [x] Unit-тесты сервисов — MessageCooldownService, LevelService (был), XpFormula, ChoiceOptionParser
|
||||||
|
- [x] Unit-тесты value objects — UserStats (был), LevelProgress, LevelUpResult, BotConfig
|
||||||
|
- [x] Тесты embed — EmbedFactory (builder chaining), EmbedTemplates (factory methods)
|
||||||
|
- [x] Тесты форматирования — LeaderboardFormatter
|
||||||
|
- [ ] Unit-тесты на команды (с моками JDA)
|
||||||
|
- [ ] Smoke-тест: все команды регистрируются без коллизий
|
||||||
|
- [ ] Тесты на RankCardGenerator (изолированные, без сети)
|
||||||
|
- [ ] Тесты на EventDispatcher (маршрутизация, rate limiting)
|
||||||
|
|
||||||
|
### Этап 6: Инфраструктура ✅
|
||||||
|
- [x] Dockerfile (multi-stage)
|
||||||
|
- [x] docker-compose.yml
|
||||||
|
- [x] Статический анализ: checkstyle / spotbugs
|
||||||
|
- [x] Версионный каталог: `gradle/libs.versions.toml`
|
||||||
|
- [x] HikariCP вместо голого JDBC
|
||||||
|
|
||||||
|
### Этап 7: Кодстайл
|
||||||
|
- [x] EmbedFactory: `set*` → `with*` для builder
|
||||||
|
- [ ] DatabaseMigration: оценить нужность
|
||||||
|
- [x] HelpMenuHandler: переместить в feature/help/
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🐛 Известные проблемы (Bugs to Fix)
|
## 🚀 Новые фичи (Roadmap)
|
||||||
|
|
||||||
### Критические:
|
## 🚀 Экономика — план реализации (в работе)
|
||||||
- [ ] Проверить корректность начисления XP в войсе
|
|
||||||
- [ ] Убедиться что VoiceTracker правильно отслеживает время
|
|
||||||
|
|
||||||
### Некритические:
|
### Модель данных
|
||||||
- [ ] Удалить неиспользуемый logger в LevelSystem.java
|
|
||||||
- [ ] Убрать ненужный @SuppressWarnings в LevelSystem.java
|
|
||||||
|
|
||||||
---
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS economy (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
cash INTEGER NOT NULL DEFAULT 0, -- на руках
|
||||||
|
bank INTEGER NOT NULL DEFAULT 0, -- в банке
|
||||||
|
total_earned INTEGER NOT NULL DEFAULT 0,
|
||||||
|
total_spent INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_daily TEXT, -- ISO date
|
||||||
|
daily_streak INTEGER DEFAULT 0,
|
||||||
|
fishing_luck INTEGER DEFAULT 0, -- stat lvl 0-5
|
||||||
|
efficiency INTEGER DEFAULT 0, -- stat lvl 0-5
|
||||||
|
last_fish TEXT, -- ISO datetime
|
||||||
|
job TEXT, -- название работы
|
||||||
|
last_work TEXT -- ISO datetime
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
## 📚 Технические детали
|
### Заработок
|
||||||
|
|
||||||
### Зависимости:
|
| Механика | Доход | Кулдаун |
|
||||||
- JDA 5.0.0 (уже есть)
|
|---|---|---|
|
||||||
- Guice для DI (уже есть)
|
| Сообщение | 1–3 LC | 60с |
|
||||||
- Gson для JSON (уже есть)
|
| Голос | 1 LC / 2 мин | 2 мин |
|
||||||
|
| Ежедневно | 50 LC + streak ×1.5 на 7й день | 24ч |
|
||||||
|
| +rep (полученная) | 10 LC | — |
|
||||||
|
| Рыбалка | base × speed_mul × (1 + fishing_luck×0.01) | 2ч |
|
||||||
|
| Работа | random(range) × (1 + efficiency×0.01) | 6ч |
|
||||||
|
| Блэкджек | ×1.5 при победе | — |
|
||||||
|
| Слоты | ×0–×50 | — |
|
||||||
|
|
||||||
### Конфигурация (добавить в config):
|
### Миниигры
|
||||||
```yaml
|
|
||||||
voice:
|
**Рыбалка** (`!fish easy/normal/hard`)
|
||||||
trigger_channel_id: 1234567890
|
- Сетка N×N (emoji + 🌊), бесплатно, неправильный ответ = 0
|
||||||
category_id: 1234567890
|
- Множитель скорости: <50% времени = ×1.5, <30% = ×2.0
|
||||||
text_channel_id: 1234567890
|
- Стат `fishing_luck` (макс 5, стоимость 800×lvl)
|
||||||
auto_delete_empty: true
|
|
||||||
|
**Блэкджек** (`!bj <bet>`)
|
||||||
counter:
|
- Дилер стоит на 16, ничья = проигрыш, BJ = ×1.5
|
||||||
channel_id: 1234567890
|
- Кнопки Hit/Stand
|
||||||
update_interval: 300 # секунды
|
|
||||||
format: "【👥】Участники: {count}"
|
**Слоты** (`!slot <bet>`)
|
||||||
|
- 🍒🍒🍒 ×3 (10%), 🔔🔔🔔 ×5 (5%), 💎💎💎 ×10 (2%), 👑👑👑 ×15 (1%), ⚡⚡⚡ ×50 (0.1%)
|
||||||
|
|
||||||
|
**Работы** (`!job list`, `!job join`, `!work`)
|
||||||
|
| Работа | Ур. | Доход |
|
||||||
|
|---|---|---|
|
||||||
|
| 🪓 Лесоруб | — | 15–30 |
|
||||||
|
| ⛏️ Шахтёр | — | 20–40 |
|
||||||
|
| 🎣 Рыбак | — | 25–50 |
|
||||||
|
| 🍳 Повар | 15 | 30–60 |
|
||||||
|
| 💻 Программист | 20 | 40–80 |
|
||||||
|
| 🧑🚀 Космонавт | 35 | 50–100 |
|
||||||
|
|
||||||
|
Стат `efficiency` (макс 5, стоимость 800×lvl)
|
||||||
|
|
||||||
|
**Rob** (`!rob @user`)
|
||||||
|
- 24ч кулдаун, 40% успех, крадёт до 15% cash (макс 10к)
|
||||||
|
- Провал: штраф 10% cash (макс 5к) → жертве
|
||||||
|
- Банк не трогается
|
||||||
|
|
||||||
|
**Банк** (`!bank deposit/withdraw/balance`)
|
||||||
|
- Без процентов, защита от rob
|
||||||
|
|
||||||
|
### Структура пакета
|
||||||
|
|
||||||
|
```
|
||||||
|
feature/economy/
|
||||||
|
EconomyModule.java
|
||||||
|
EconomyCommandRegistrar.java
|
||||||
|
domain/
|
||||||
|
EconomyAccount.java
|
||||||
|
EconomyTransaction.java
|
||||||
|
Job.java
|
||||||
|
SlotCombination.java
|
||||||
|
repository/
|
||||||
|
EconomyRepository.java
|
||||||
|
SqlEconomyRepository.java
|
||||||
|
service/
|
||||||
|
EconomyService.java
|
||||||
|
FishingService.java
|
||||||
|
BlackjackService.java
|
||||||
|
SlotService.java
|
||||||
|
JobService.java
|
||||||
|
command/
|
||||||
|
core/ (Balance, Baltop, Pay, Daily)
|
||||||
|
minigame/ (Fish, Blackjack, Slot, Job, Work)
|
||||||
|
social/ (Rob)
|
||||||
|
bank/ (Bank)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✅ Чеклист перед релизом
|
### Приоритет 1
|
||||||
|
- [ ] Система достижений (Achievements)
|
||||||
|
- [x] Система экономики (Economy) — **в работе**
|
||||||
|
|
||||||
- [ ] Все функции протестированы
|
### Приоритет 2
|
||||||
- [ ] Логирование работает корректно
|
- [x] Авто-модерация (3 варна = авто-мут на 1ч)
|
||||||
- [ ] Нет утечек памяти (проверить scheduler'ы)
|
- [x] Музыка (lavaplayer: play, skip, stop, queue, np, volume, leave)
|
||||||
- [ ] Graceful shutdown для всех scheduler'ов
|
- [x] ~~Музыка — поиск с выбором (yt/sc/list + выбор номера)~~
|
||||||
- [ ] Документация обновлена
|
- [x] Server Stats (участники, каналы, роли, бусты)
|
||||||
- [ ] FEATURES.md актуализирован
|
- [ ] Level Roles (роли за уровни)
|
||||||
|
- [ ] Красивые level-up embed с прогресс-баром
|
||||||
|
|
||||||
|
### Приоритет 3
|
||||||
|
- [ ] Статистика сервера — графики, CSV export
|
||||||
|
- [ ] Система квестов
|
||||||
|
- [ ] Wiki по установке и командам
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Дата создания:** 2026-05-11
|
**Версия:** 2.2 | **Последнее обновление:** 2026-06-20 | **Java 21 ✅, God Constructor ✅, Critical баги ✅, Economy 🚧**
|
||||||
**Версия:** 1.0
|
|
||||||
**Автор:** Loki Bot Team
|
|
||||||
|
|
|
||||||
65
build.gradle
65
build.gradle
|
|
@ -1,6 +1,8 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'application'
|
id 'application'
|
||||||
|
id 'checkstyle'
|
||||||
|
alias(libs.plugins.spotbugs)
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'creator.loki'
|
group = 'creator.loki'
|
||||||
|
|
@ -11,47 +13,33 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// JDA - Discord API
|
implementation(libs.jda) {
|
||||||
implementation('net.dv8tion:JDA:5.0.0-beta.20') {
|
|
||||||
exclude module: 'opus-java'
|
exclude module: 'opus-java'
|
||||||
}
|
}
|
||||||
|
implementation libs.lavaplayer
|
||||||
|
implementation libs.dotenv
|
||||||
|
implementation libs.slf4j.api
|
||||||
|
implementation libs.logback.classic
|
||||||
|
implementation libs.gson
|
||||||
|
implementation libs.caffeine
|
||||||
|
implementation libs.guice
|
||||||
|
implementation libs.sqlite.jdbc
|
||||||
|
implementation libs.hikaricp
|
||||||
|
|
||||||
// LavaPlayer для голосового TTS
|
testImplementation libs.junit.jupiter
|
||||||
implementation 'dev.arbjerg:lavaplayer:2.2.2'
|
testImplementation libs.mockito.core
|
||||||
|
testImplementation libs.mockito.junit.jupiter
|
||||||
// dotenv для загрузки .env
|
testRuntimeOnly libs.junit.platform.launcher
|
||||||
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 {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile) {
|
tasks.withType(JavaCompile) {
|
||||||
options.encoding = 'UTF-8'
|
options.encoding = 'UTF-8'
|
||||||
|
options.compilerArgs << '-Xlint:all'
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|
@ -59,13 +47,13 @@ test {
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass = 'creator.loki.App'
|
mainClass = 'creator.loki.bootstrap.App'
|
||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes(
|
attributes(
|
||||||
'Main-Class': 'creator.loki.App'
|
'Main-Class': 'creator.loki.bootstrap.App'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
|
@ -73,3 +61,14 @@ jar {
|
||||||
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
|
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkstyle {
|
||||||
|
configFile = file("${rootDir}/config/checkstyle/checkstyle.xml")
|
||||||
|
maxErrors = 999
|
||||||
|
maxWarnings = 999
|
||||||
|
}
|
||||||
|
|
||||||
|
spotbugs {
|
||||||
|
excludeFilter = file("${rootDir}/config/spotbugs/exclude.xml")
|
||||||
|
ignoreFailures = true
|
||||||
|
}
|
||||||
|
|
|
||||||
90
config/checkstyle/checkstyle.xml
Normal file
90
config/checkstyle/checkstyle.xml
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE module PUBLIC
|
||||||
|
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||||
|
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||||
|
<module name="Checker">
|
||||||
|
<property name="charset" value="UTF-8"/>
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
<property name="fileExtensions" value="java"/>
|
||||||
|
|
||||||
|
<module name="BeforeExecutionExclusionFileFilter">
|
||||||
|
<property name="fileNamePattern" value="module\-info\.java$"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="FileTabCharacter">
|
||||||
|
<property name="eachLine" value="true"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="LineLength">
|
||||||
|
<property name="max" value="150"/>
|
||||||
|
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="RegexpSingleline">
|
||||||
|
<property name="format" value="\s+$"/>
|
||||||
|
<property name="message" value="Line has trailing whitespace."/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="TreeWalker">
|
||||||
|
<module name="PackageName">
|
||||||
|
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="TypeName">
|
||||||
|
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, RECORD_DEF"/>
|
||||||
|
<property name="format" value="^[A-Z][a-zA-Z0-9]*$"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="MethodName">
|
||||||
|
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="ConstantName">
|
||||||
|
<property name="format" value="^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$|^logger$|^LOGGER$"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="LocalVariableName">
|
||||||
|
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
|
||||||
|
<property name="allowOneCharVarInForLoop" value="true"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="MemberName">
|
||||||
|
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="ParameterName">
|
||||||
|
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="LambdaParameterName">
|
||||||
|
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="LeftCurly">
|
||||||
|
<property name="option" value="eol"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="RightCurly">
|
||||||
|
<property name="option" value="same"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="WhitespaceAround">
|
||||||
|
<property name="allowEmptyConstructors" value="true"/>
|
||||||
|
<property name="allowEmptyMethods" value="true"/>
|
||||||
|
<property name="allowEmptyTypes" value="true"/>
|
||||||
|
<property name="allowEmptyLoops" value="true"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="NoWhitespaceAfter"/>
|
||||||
|
<module name="NoWhitespaceBefore"/>
|
||||||
|
|
||||||
|
<module name="Indentation">
|
||||||
|
<property name="basicOffset" value="4"/>
|
||||||
|
<property name="braceAdjustment" value="0"/>
|
||||||
|
<property name="caseIndent" value="4"/>
|
||||||
|
<property name="throwsIndent" value="4"/>
|
||||||
|
<property name="lineWrappingIndentation" value="4"/>
|
||||||
|
<property name="arrayInitIndent" value="4"/>
|
||||||
|
</module>
|
||||||
|
</module>
|
||||||
|
</module>
|
||||||
6
config/spotbugs/exclude.xml
Normal file
6
config/spotbugs/exclude.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<FindBugsFilter
|
||||||
|
xmlns="https://github.com/spotbugs/filter"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="https://github.com/spotbugs/filter https://raw.githubusercontent.com/spotbugs/spotbugs/7.0.0/spotbugs/etc/findbugsfilter.xsd">
|
||||||
|
</FindBugsFilter>
|
||||||
21
docker-compose.yml
Normal file
21
docker-compose.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
bot:
|
||||||
|
build: .
|
||||||
|
container_name: loki-bot
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8712:8712"
|
||||||
|
environment:
|
||||||
|
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN}
|
||||||
|
BOT_PREFIX: ${BOT_PREFIX:-!}
|
||||||
|
BOT_ACTIVITY: ${BOT_ACTIVITY:-Используй !help}
|
||||||
|
HEALTH_HTTP_PORT: ${HEALTH_HTTP_PORT:-8712}
|
||||||
|
SUPER_ADMIN_ID: ${SUPER_ADMIN_ID}
|
||||||
|
DB_PATH: /app/data/bot.db
|
||||||
|
volumes:
|
||||||
|
- bot-data:/app/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
bot-data:
|
||||||
40
gradle/libs.versions.toml
Normal file
40
gradle/libs.versions.toml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[versions]
|
||||||
|
jda = "5.0.0-beta.20"
|
||||||
|
lavaplayer = "2.2.2"
|
||||||
|
dotenv = "3.0.0"
|
||||||
|
slf4j = "2.0.12"
|
||||||
|
logback = "1.5.3"
|
||||||
|
gson = "2.10.1"
|
||||||
|
caffeine = "3.1.8"
|
||||||
|
guice = "7.0.0"
|
||||||
|
sqlite = "3.45.1.0"
|
||||||
|
hikaricp = "5.1.0"
|
||||||
|
junit = "5.10.2"
|
||||||
|
mockito = "5.11.0"
|
||||||
|
junitPlatformLauncher = "1.10.2"
|
||||||
|
checkstyle = "10.21.4"
|
||||||
|
spotbugs = "6.1.7"
|
||||||
|
spotbugsPlugin = "6.1.7"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
jda = { module = "net.dv8tion:JDA", version.ref = "jda" }
|
||||||
|
lavaplayer = { module = "dev.arbjerg:lavaplayer", version.ref = "lavaplayer" }
|
||||||
|
dotenv = { module = "io.github.cdimascio:dotenv-java", version.ref = "dotenv" }
|
||||||
|
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
||||||
|
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||||
|
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||||
|
caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" }
|
||||||
|
guice = { module = "com.google.inject:guice", version.ref = "guice" }
|
||||||
|
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
|
||||||
|
hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikaricp" }
|
||||||
|
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||||
|
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
|
||||||
|
mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
|
||||||
|
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junitPlatformLauncher" }
|
||||||
|
checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" }
|
||||||
|
spotbugs = { module = "com.github.spotbugs:spotbugs", version.ref = "spotbugs" }
|
||||||
|
spotbugs-annotations = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "spotbugs" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
checkstyle = { id = "checkstyle", version = "" }
|
||||||
|
spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugsPlugin" }
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
||||||
120
gradlew
vendored
Normal file
120
gradlew
vendored
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld -- "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NonStop* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stracks://the current
|
||||||
|
# application classpath, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
|
|
@ -1 +1,7 @@
|
||||||
rootProject.name = 'discord-bot'
|
rootProject.name = 'discord-bot'
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,50 @@
|
||||||
package creator.loki;
|
package creator.loki.bootstrap;
|
||||||
|
|
||||||
import com.google.inject.Guice;
|
import com.google.inject.Guice;
|
||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
import creator.loki.core.BotInitializer;
|
import creator.loki.core.BotInitializer;
|
||||||
import creator.loki.di.BotModule;
|
import creator.loki.di.BotModule;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class App {
|
public class App {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(App.class);
|
private static final Logger logger = LoggerFactory.getLogger(App.class);
|
||||||
private static volatile boolean shutdownRequested = false;
|
private static volatile boolean shutdownRequested = false;
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
// Регистрируем shutdown hook ДО создания бота
|
// Регистрируем shutdown hook ДО создания бота
|
||||||
Thread shutdownHook = new Thread(() -> {
|
Thread shutdownHook = new Thread(() -> {
|
||||||
if (!shutdownRequested) {
|
if (!shutdownRequested) {
|
||||||
shutdownRequested = true;
|
shutdownRequested = true;
|
||||||
logger.info("========================================");
|
logger.info("========================================");
|
||||||
logger.info("Получен сигнал завершения");
|
logger.info("Получен сигнал завершения");
|
||||||
logger.info("Выполняется graceful shutdown...");
|
logger.info("Выполняется graceful shutdown...");
|
||||||
logger.info("========================================");
|
logger.info("========================================");
|
||||||
}
|
}
|
||||||
}, "ShutdownHook");
|
}, "ShutdownHook");
|
||||||
|
|
||||||
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info("Запуск Discord бота");
|
logger.info("Запуск Discord бота");
|
||||||
|
|
||||||
// Создаём Guice Injector с модулем конфигурации
|
// Создаём Guice Injector с модулем конфигурации
|
||||||
Injector injector = Guice.createInjector(new BotModule());
|
Injector injector = Guice.createInjector(new BotModule());
|
||||||
|
|
||||||
// Получаем BotInitializer с автоматически внедрёнными зависимостями
|
// Получаем BotInitializer с автоматически внедрёнными зависимостями
|
||||||
BotInitializer initializer = injector.getInstance(BotInitializer.class);
|
BotInitializer initializer = injector.getInstance(BotInitializer.class);
|
||||||
|
|
||||||
// Запускаем бота
|
// Запускаем бота
|
||||||
JDA jda = initializer.start();
|
JDA jda = initializer.start();
|
||||||
|
|
||||||
// Ждём полной загрузки
|
// Ждём полной загрузки
|
||||||
jda.awaitReady();
|
jda.awaitReady();
|
||||||
logger.info("Бот полностью готов к работе!");
|
logger.info("Бот полностью готов к работе!");
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Критическая ошибка при запуске бота", e);
|
logger.error("Критическая ошибка при запуске бота", e);
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
package creator.loki.listener;
|
package creator.loki.bootstrap;
|
||||||
|
|
||||||
import net.dv8tion.jda.api.events.session.ReadyEvent;
|
import com.google.inject.Singleton;
|
||||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
import net.dv8tion.jda.api.events.session.ReadyEvent;
|
||||||
import org.slf4j.Logger;
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
public class ReadyListener extends ListenerAdapter {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ReadyListener.class);
|
@Singleton
|
||||||
|
public class ReadyListener extends ListenerAdapter {
|
||||||
@Override
|
private static final Logger logger = LoggerFactory.getLogger(ReadyListener.class);
|
||||||
public void onReady(ReadyEvent event) {
|
|
||||||
logger.info("Бот {} успешно запущен!", event.getJDA().getSelfUser().getName());
|
@Override
|
||||||
logger.info("Подключен к {} серверам", event.getGuildTotalCount());
|
public void onReady(ReadyEvent event) {
|
||||||
}
|
logger.info("Бот {} успешно запущен!", event.getJDA().getSelfUser().getName());
|
||||||
}
|
logger.info("Подключен к {} серверам", event.getGuildTotalCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
package creator.loki.command;
|
package creator.loki.command;
|
||||||
|
|
||||||
public interface Command {
|
public interface Command {
|
||||||
String getName();
|
String getName();
|
||||||
String getDescription();
|
String getDescription();
|
||||||
|
|
||||||
default String getUsage() {
|
default String getUsage() {
|
||||||
return getName();
|
return getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
default String getCategory() {
|
default String getCategory() {
|
||||||
return "Общие";
|
return "Общие";
|
||||||
}
|
}
|
||||||
|
|
||||||
void execute(CommandContext context);
|
void execute(CommandContext context);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
src/main/java/creator/loki/command/CommandCategories.java
Normal file
12
src/main/java/creator/loki/command/CommandCategories.java
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
package creator.loki.command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Имена категорий для {@link Command#getCategory()} (справка, селектор help).
|
||||||
|
*/
|
||||||
|
public final class CommandCategories {
|
||||||
|
|
||||||
|
/** Команды с GIF (nekos.life): hug, slap, poke и т.п. */
|
||||||
|
public static final String GIF_INTERACTIVE = "GIF-интерактив";
|
||||||
|
|
||||||
|
private CommandCategories() {}
|
||||||
|
}
|
||||||
|
|
@ -1,44 +1,44 @@
|
||||||
package creator.loki.command;
|
package creator.loki.command;
|
||||||
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
import net.dv8tion.jda.api.entities.Message;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class CommandContext {
|
public class CommandContext {
|
||||||
private final Message message;
|
private final Message message;
|
||||||
private final List<String> args;
|
private final List<String> args;
|
||||||
private final MessageChannel channel;
|
private final MessageChannel channel;
|
||||||
private final Member author;
|
private final Member author;
|
||||||
private final Guild guild;
|
private final Guild guild;
|
||||||
|
|
||||||
public CommandContext(Message message, List<String> args, MessageChannel channel, Member author, Guild guild) {
|
public CommandContext(Message message, List<String> args, MessageChannel channel, Member author, Guild guild) {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.args = args;
|
this.args = args;
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
this.author = author;
|
this.author = author;
|
||||||
this.guild = guild;
|
this.guild = guild;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Message getMessage() {
|
public Message getMessage() {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getArgs() {
|
public List<String> getArgs() {
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageChannel getChannel() {
|
public MessageChannel getChannel() {
|
||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Member getAuthor() {
|
public Member getAuthor() {
|
||||||
return author;
|
return author;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guild getGuild() {
|
public Guild getGuild() {
|
||||||
return guild;
|
return guild;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
package creator.loki.command.impl.entertainment.interaction.negative;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
|
||||||
import creator.loki.command.CommandContext;
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.core.security.CooldownManager;
|
|
||||||
import creator.loki.integration.nekos.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
package creator.loki.command.impl.entertainment.interaction.negative;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
|
||||||
import creator.loki.command.CommandContext;
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.core.security.CooldownManager;
|
|
||||||
import creator.loki.integration.nekos.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
package creator.loki.command.impl.entertainment.interaction.positive;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
|
||||||
import creator.loki.command.CommandContext;
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.core.security.CooldownManager;
|
|
||||||
import creator.loki.integration.nekos.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
package creator.loki.command.impl.entertainment.interaction.negative;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
|
||||||
import creator.loki.command.CommandContext;
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.core.security.CooldownManager;
|
|
||||||
import creator.loki.integration.nekos.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
package creator.loki.command.impl.entertainment.interaction.positive;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
|
||||||
import creator.loki.command.CommandContext;
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.core.security.CooldownManager;
|
|
||||||
import creator.loki.integration.nekos.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
package creator.loki.command.impl.entertainment.interaction.positive;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
|
||||||
import creator.loki.command.CommandContext;
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.integration.nekos.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
package creator.loki.command.impl.entertainment.interaction.positive;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
|
||||||
import creator.loki.command.CommandContext;
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.core.security.CooldownManager;
|
|
||||||
import creator.loki.integration.nekos.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
package creator.loki.command.impl.entertainment.interaction.positive;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
|
||||||
import creator.loki.command.CommandContext;
|
|
||||||
import creator.loki.core.security.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getCategory() {
|
|
||||||
return "Модерация";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
package creator.loki.command.impl.stats;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
|
||||||
import creator.loki.command.CommandContext;
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.feature.level.domain.LevelProgress;
|
|
||||||
import creator.loki.feature.level.domain.UserStats;
|
|
||||||
import creator.loki.level.LevelSystem;
|
|
||||||
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);
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,159 +0,0 @@
|
||||||
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.registry.CommandRegistry;
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import net.dv8tion.jda.api.Permission;
|
|
||||||
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;
|
|
||||||
|
|
||||||
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<>();
|
|
||||||
private static final Map<String, String> CATEGORY_DESCRIPTIONS = new HashMap<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
CATEGORY_EMOJIS.put("Общие", "📋");
|
|
||||||
CATEGORY_EMOJIS.put("Роли", "🎭");
|
|
||||||
CATEGORY_EMOJIS.put("Развлечения", "🎮");
|
|
||||||
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));
|
|
||||||
CATEGORY_COLORS.put("Статистика", new Color(46, 204, 113));
|
|
||||||
CATEGORY_COLORS.put("Утилиты", new Color(52, 152, 219));
|
|
||||||
CATEGORY_COLORS.put("Настройка", new Color(149, 165, 166));
|
|
||||||
|
|
||||||
CATEGORY_DESCRIPTIONS.put("Общие", "Основные команды бота");
|
|
||||||
CATEGORY_DESCRIPTIONS.put("Роли", "Управление кастомными ролями");
|
|
||||||
CATEGORY_DESCRIPTIONS.put("Развлечения", "Игры и взаимодействия");
|
|
||||||
CATEGORY_DESCRIPTIONS.put("Модерация", "Команды модерации сервера");
|
|
||||||
CATEGORY_DESCRIPTIONS.put("Статистика", "Уровни, репутация и статистика");
|
|
||||||
CATEGORY_DESCRIPTIONS.put("Утилиты", "Полезные инструменты");
|
|
||||||
CATEGORY_DESCRIPTIONS.put("Настройка", "Настройка систем бота (только админы)");
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
// Главная страница
|
|
||||||
EmbedFactory embed = createMainPage(context, commandsByCategory);
|
|
||||||
|
|
||||||
// Создаём селектор категорий
|
|
||||||
StringSelectMenu.Builder menuBuilder = StringSelectMenu.create("help:category")
|
|
||||||
.setPlaceholder("📂 Выберите категорию");
|
|
||||||
|
|
||||||
boolean isAdmin = context.getAuthor() != null &&
|
|
||||||
context.getAuthor().hasPermission(Permission.ADMINISTRATOR);
|
|
||||||
|
|
||||||
commandsByCategory.keySet().stream()
|
|
||||||
.sorted()
|
|
||||||
.forEach(category -> {
|
|
||||||
// Скрываем категорию "Настройка" для не-админов
|
|
||||||
if (category.equals("Настройка") && !isAdmin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String emoji = CATEGORY_EMOJIS.getOrDefault(category, "📌");
|
|
||||||
String description = CATEGORY_DESCRIPTIONS.getOrDefault(category, "Команды категории");
|
|
||||||
int count = commandsByCategory.get(category).size();
|
|
||||||
|
|
||||||
menuBuilder.addOption(
|
|
||||||
category,
|
|
||||||
"category:" + category,
|
|
||||||
description + " (" + count + " команд)",
|
|
||||||
net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode(emoji)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
context.getChannel()
|
|
||||||
.sendMessageEmbeds(embed.build())
|
|
||||||
.addActionRow(menuBuilder.build())
|
|
||||||
.queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private EmbedFactory createMainPage(CommandContext context, Map<String, List<Command>> commandsByCategory) {
|
|
||||||
int totalCommands = commandsByCategory.values().stream()
|
|
||||||
.mapToInt(List::size)
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
boolean isAdmin = context.getAuthor() != null &&
|
|
||||||
context.getAuthor().hasPermission(Permission.ADMINISTRATOR);
|
|
||||||
|
|
||||||
StringBuilder categoriesText = new StringBuilder();
|
|
||||||
commandsByCategory.keySet().stream()
|
|
||||||
.sorted()
|
|
||||||
.forEach(category -> {
|
|
||||||
// Скрываем категорию "Настройка" для не-админов
|
|
||||||
if (category.equals("Настройка") && !isAdmin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String emoji = CATEGORY_EMOJIS.getOrDefault(category, "📌");
|
|
||||||
int count = commandsByCategory.get(category).size();
|
|
||||||
categoriesText.append(emoji).append(" **").append(category)
|
|
||||||
.append("** — ").append(count).append(" команд\n");
|
|
||||||
});
|
|
||||||
|
|
||||||
return EmbedFactory.create()
|
|
||||||
.setTitle("📚 Справка по командам Loki Bot")
|
|
||||||
.setDescription(
|
|
||||||
"Привет! Я многофункциональный Discord бот с системой уровней, " +
|
|
||||||
"личными голосовыми каналами и множеством развлекательных команд.\n\n" +
|
|
||||||
"**Префикс команд:** `" + config.getPrefix() + "`\n" +
|
|
||||||
"**Всего команд:** " + totalCommands + "\n\n" +
|
|
||||||
"**📂 Категории команд:**\n" + categoriesText.toString() + "\n" +
|
|
||||||
"Используй селектор ниже для просмотра команд категории."
|
|
||||||
)
|
|
||||||
.setColor(new Color(88, 101, 242))
|
|
||||||
.setThumbnail(context.getGuild() != null ? context.getGuild().getIconUrl() : null)
|
|
||||||
.addField("💡 Как использовать",
|
|
||||||
"Большинство команд используют префикс `!`\n" +
|
|
||||||
"Пример: `!rank`, `!ship @user1 @user2`\n\n" +
|
|
||||||
"Некоторые важные команды используют `/`\n" +
|
|
||||||
"Пример: `/voice-setup`, `/counter-setup`",
|
|
||||||
false)
|
|
||||||
.setFooter(
|
|
||||||
"Запросил: " + context.getAuthor().getUser().getName(),
|
|
||||||
context.getAuthor().getUser().getAvatarUrl()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getCategory() {
|
|
||||||
return "Утилиты";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
package creator.loki.command.impl.utility.system;
|
|
||||||
|
|
||||||
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 "Система";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +1,26 @@
|
||||||
package creator.loki.config;
|
package creator.loki.config;
|
||||||
|
|
||||||
public class BotConfig {
|
public class BotConfig {
|
||||||
private final String token;
|
private final String token;
|
||||||
private final String prefix;
|
private final String prefix;
|
||||||
private final String activity;
|
private final String activity;
|
||||||
|
private final int healthHttpPort;
|
||||||
public BotConfig(String token, String prefix, String activity) {
|
private final String superAdminId;
|
||||||
this.token = token;
|
private final String dbPath;
|
||||||
this.prefix = prefix;
|
|
||||||
this.activity = activity;
|
public BotConfig(String token, String prefix, String activity, int healthHttpPort, String superAdminId, String dbPath) {
|
||||||
}
|
this.token = token;
|
||||||
|
this.prefix = prefix;
|
||||||
public String getToken() {
|
this.activity = activity;
|
||||||
return token;
|
this.healthHttpPort = healthHttpPort;
|
||||||
}
|
this.superAdminId = superAdminId;
|
||||||
|
this.dbPath = dbPath;
|
||||||
public String getPrefix() {
|
}
|
||||||
return prefix;
|
|
||||||
}
|
public String getToken() { return token; }
|
||||||
|
public String getPrefix() { return prefix; }
|
||||||
public String getActivity() {
|
public String getActivity() { return activity; }
|
||||||
return activity;
|
public int getHealthHttpPort() { return healthHttpPort; }
|
||||||
}
|
public String getSuperAdminId() { return superAdminId; }
|
||||||
}
|
public String getDbPath() { return dbPath; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,67 @@
|
||||||
package creator.loki.config;
|
package creator.loki.config;
|
||||||
|
|
||||||
import io.github.cdimascio.dotenv.Dotenv;
|
import io.github.cdimascio.dotenv.Dotenv;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public final class ConfigLoader {
|
public final class ConfigLoader {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ConfigLoader.class);
|
private static final Logger logger = LoggerFactory.getLogger(ConfigLoader.class);
|
||||||
|
|
||||||
private ConfigLoader() {
|
private ConfigLoader() {
|
||||||
// Запрет создания экземпляров
|
}
|
||||||
}
|
|
||||||
|
private static int parsePort(String raw, String key) {
|
||||||
public static BotConfig load() {
|
if (raw == null || raw.isBlank()) return 0;
|
||||||
Dotenv dotenv = Dotenv.configure()
|
try {
|
||||||
.ignoreIfMissing()
|
int port = Integer.parseInt(raw.trim());
|
||||||
.load();
|
if (port < 0 || port > 65535) {
|
||||||
|
throw new IllegalArgumentException("порт вне диапазона 0..65535");
|
||||||
String token = dotenv.get("DISCORD_BOT_TOKEN");
|
}
|
||||||
if (token == null || token.isBlank()) {
|
return port;
|
||||||
throw new IllegalStateException(
|
} catch (NumberFormatException e) {
|
||||||
"Токен бота не найден! Установите переменную окружения DISCORD_BOT_TOKEN или создайте .env файл"
|
throw new IllegalStateException(
|
||||||
);
|
"Некорректный " + key + ": \"" + raw + "\". Ожидается число 0..65535.",
|
||||||
}
|
e
|
||||||
|
);
|
||||||
String prefix = dotenv.get("BOT_PREFIX");
|
}
|
||||||
if (prefix == null || prefix.isBlank()) {
|
}
|
||||||
prefix = "!";
|
|
||||||
}
|
public static BotConfig load() {
|
||||||
|
Dotenv dotenv = Dotenv.configure()
|
||||||
String activity = dotenv.get("BOT_ACTIVITY");
|
.ignoreIfMissing()
|
||||||
if (activity == null || activity.isBlank()) {
|
.load();
|
||||||
activity = "Используй !help";
|
|
||||||
}
|
String token = dotenv.get("DISCORD_BOT_TOKEN");
|
||||||
|
if (token == null || token.isBlank()) {
|
||||||
logger.info("Конфигурация загружена успешно");
|
throw new IllegalStateException(
|
||||||
return new BotConfig(token, prefix, activity);
|
"Токен бота не найден! Установите переменную окружения 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";
|
||||||
|
}
|
||||||
|
|
||||||
|
int healthHttpPort = parsePort(dotenv.get("HEALTH_HTTP_PORT"), "HEALTH_HTTP_PORT");
|
||||||
|
|
||||||
|
String superAdminId = dotenv.get("SUPER_ADMIN_ID");
|
||||||
|
if (superAdminId == null || superAdminId.isBlank()) {
|
||||||
|
superAdminId = "1414975197269987471";
|
||||||
|
logger.warn("SUPER_ADMIN_ID не задан, используется значение по умолчанию");
|
||||||
|
}
|
||||||
|
|
||||||
|
String dbPath = dotenv.get("DB_PATH");
|
||||||
|
if (dbPath == null || dbPath.isBlank()) {
|
||||||
|
dbPath = "data/bot.db";
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Конфигурация загружена успешно");
|
||||||
|
return new BotConfig(token, prefix, activity, healthHttpPort, superAdminId, dbPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,11 @@
|
||||||
package creator.loki.core;
|
package creator.loki.core;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
import creator.loki.command.impl.setup.LogSetupCommand;
|
|
||||||
import creator.loki.command.impl.stats.leaderboard.LeaderboardButtonListener;
|
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
import creator.loki.core.EventDispatcher;
|
|
||||||
import creator.loki.core.registry.CommandRegistry;
|
|
||||||
import creator.loki.core.registry.PrefixCommandRegistrar;
|
import creator.loki.core.registry.PrefixCommandRegistrar;
|
||||||
import creator.loki.core.registry.SlashCommandRegistrar;
|
|
||||||
import creator.loki.core.registry.SlashCommandRegistry;
|
|
||||||
import creator.loki.feature.counter.CounterManager;
|
|
||||||
import creator.loki.data.JsonDataStore;
|
|
||||||
import creator.loki.database.ConfigRepository;
|
|
||||||
import creator.loki.database.Database;
|
|
||||||
import creator.loki.feature.counter.CounterSetupCommand;
|
|
||||||
import creator.loki.feature.counter.MemberCounterListener;
|
import creator.loki.feature.counter.MemberCounterListener;
|
||||||
import creator.loki.feature.level.LevelUpNotifier;
|
import creator.loki.feature.moderation.MuteScheduler;
|
||||||
import creator.loki.feature.level.service.LevelService;
|
import creator.loki.feature.metrics.HealthHttpServer;
|
||||||
import creator.loki.feature.voice.VoiceButtonHandler;
|
|
||||||
import creator.loki.feature.voice.VoiceChannelListener;
|
|
||||||
import creator.loki.feature.voice.VoiceChannelManager;
|
|
||||||
import creator.loki.feature.voice.VoiceSetupCommand;
|
|
||||||
import creator.loki.feature.level.VoiceTracker;
|
|
||||||
import creator.loki.level.LevelSystem;
|
|
||||||
import creator.loki.listener.*;
|
|
||||||
import creator.loki.listener.interaction.SlashCommandListener;
|
|
||||||
import creator.loki.metrics.MetricsCollector;
|
|
||||||
import creator.loki.profile.ProfileService;
|
|
||||||
import creator.loki.feature.role.CustomRoleManager;
|
|
||||||
import creator.loki.core.security.CooldownManager;
|
|
||||||
import creator.loki.core.security.RateLimiter;
|
|
||||||
import creator.loki.feature.voice.VoiceChannelManager;
|
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import net.dv8tion.jda.api.JDABuilder;
|
import net.dv8tion.jda.api.JDABuilder;
|
||||||
import net.dv8tion.jda.api.OnlineStatus;
|
import net.dv8tion.jda.api.OnlineStatus;
|
||||||
|
|
@ -44,118 +18,52 @@ public class BotInitializer {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(BotInitializer.class);
|
private static final Logger logger = LoggerFactory.getLogger(BotInitializer.class);
|
||||||
|
|
||||||
private final BotConfig config;
|
private final BotConfig config;
|
||||||
private final CommandRegistry registry;
|
private final PrefixCommandRegistrar registrar;
|
||||||
private final SlashCommandRegistry slashRegistry;
|
private final ListenerAggregator listenerAggregator;
|
||||||
private final EventDispatcher dispatcher;
|
private final MuteScheduler muteScheduler;
|
||||||
private final JsonDataStore dataStore;
|
|
||||||
private final CustomRoleManager roleManager;
|
|
||||||
private final CooldownManager cooldownManager;
|
|
||||||
private final RateLimiter rateLimiter;
|
|
||||||
private final MetricsCollector metrics;
|
|
||||||
private final LevelSystem levelSystem;
|
|
||||||
private final LevelService levelService;
|
|
||||||
private final VoiceTracker voiceTracker;
|
|
||||||
private final ProfileService profileService;
|
|
||||||
private final creator.loki.feature.profile.ProfileService newProfileService;
|
|
||||||
private final VoiceChannelManager voiceChannelManager;
|
|
||||||
private final CounterManager counterManager;
|
|
||||||
private final VoiceChannelListener voiceChannelListener;
|
|
||||||
private final MemberCounterListener memberCounterListener;
|
private final MemberCounterListener memberCounterListener;
|
||||||
private final Database database;
|
private final HealthHttpServer healthHttpServer;
|
||||||
private final ConfigRepository configRepo;
|
private final ShutdownManager shutdownManager;
|
||||||
private final VoiceSetupCommand voiceSetupCommand;
|
|
||||||
private final CounterSetupCommand counterSetupCommand;
|
|
||||||
private final LogSetupCommand logSetupCommand;
|
|
||||||
private final VoiceButtonHandler voiceButtonHandler;
|
|
||||||
private final ModerationLogger moderationLogger;
|
|
||||||
private final LevelUpNotifier levelUpNotifier;
|
|
||||||
private final HelpMenuHandler helpMenuHandler;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public BotInitializer(
|
public BotInitializer(
|
||||||
BotConfig config,
|
BotConfig config,
|
||||||
CommandRegistry registry,
|
PrefixCommandRegistrar registrar,
|
||||||
SlashCommandRegistry slashRegistry,
|
ListenerAggregator listenerAggregator,
|
||||||
EventDispatcher dispatcher,
|
MuteScheduler muteScheduler,
|
||||||
JsonDataStore dataStore,
|
MemberCounterListener memberCounterListener,
|
||||||
CustomRoleManager roleManager,
|
HealthHttpServer healthHttpServer,
|
||||||
CooldownManager cooldownManager,
|
ShutdownManager shutdownManager
|
||||||
RateLimiter rateLimiter,
|
|
||||||
MetricsCollector metrics,
|
|
||||||
LevelSystem levelSystem,
|
|
||||||
LevelService levelService,
|
|
||||||
VoiceTracker voiceTracker,
|
|
||||||
ProfileService profileService,
|
|
||||||
creator.loki.feature.profile.ProfileService newProfileService,
|
|
||||||
VoiceChannelManager voiceChannelManager,
|
|
||||||
CounterManager counterManager,
|
|
||||||
Database database,
|
|
||||||
ConfigRepository configRepo,
|
|
||||||
LevelUpNotifier levelUpNotifier
|
|
||||||
) {
|
) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.registry = registry;
|
this.registrar = registrar;
|
||||||
this.slashRegistry = slashRegistry;
|
this.listenerAggregator = listenerAggregator;
|
||||||
this.dispatcher = dispatcher;
|
this.muteScheduler = muteScheduler;
|
||||||
this.dataStore = dataStore;
|
this.memberCounterListener = memberCounterListener;
|
||||||
this.roleManager = roleManager;
|
this.healthHttpServer = healthHttpServer;
|
||||||
this.cooldownManager = cooldownManager;
|
this.shutdownManager = shutdownManager;
|
||||||
this.rateLimiter = rateLimiter;
|
|
||||||
this.metrics = metrics;
|
|
||||||
this.levelSystem = levelSystem;
|
|
||||||
this.levelService = levelService;
|
|
||||||
this.voiceTracker = voiceTracker;
|
|
||||||
this.profileService = profileService;
|
|
||||||
this.newProfileService = newProfileService;
|
|
||||||
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);
|
|
||||||
this.helpMenuHandler = new HelpMenuHandler(registry, config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public JDA start() {
|
public JDA start() {
|
||||||
logger.info("Инициализация бота...");
|
logger.info("Инициализация бота...");
|
||||||
|
registrar.registerAll();
|
||||||
// Регистрируем команды через специализированные регистраторы
|
|
||||||
registerCommands();
|
|
||||||
registerSlashCommands();
|
|
||||||
|
|
||||||
// Создаём JDA
|
|
||||||
JDA jda = createJDA();
|
JDA jda = createJDA();
|
||||||
|
try {
|
||||||
// Регистрируем slash команды в Discord
|
jda.awaitReady();
|
||||||
registerSlashCommandsInDiscord(jda);
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
// Настраиваем graceful shutdown
|
throw new RuntimeException("Бот прерван во время ожидания готовности", e);
|
||||||
setupShutdownHook(jda);
|
}
|
||||||
|
jda.updateCommands().queue();
|
||||||
|
muteScheduler.restorePending(jda);
|
||||||
|
for (var guild : jda.getGuilds()) {
|
||||||
|
memberCounterListener.startScheduler(guild);
|
||||||
|
}
|
||||||
|
healthHttpServer.start();
|
||||||
|
registerShutdownHook(jda);
|
||||||
return jda;
|
return jda;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerCommands() {
|
|
||||||
PrefixCommandRegistrar registrar = new PrefixCommandRegistrar(
|
|
||||||
registry, config, dataStore, roleManager, cooldownManager,
|
|
||||||
levelSystem, levelService, newProfileService, metrics
|
|
||||||
);
|
|
||||||
registrar.registerAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerSlashCommands() {
|
|
||||||
SlashCommandRegistrar registrar = new SlashCommandRegistrar(
|
|
||||||
slashRegistry, dataStore, metrics
|
|
||||||
);
|
|
||||||
registrar.registerAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private JDA createJDA() {
|
private JDA createJDA() {
|
||||||
return JDABuilder.createDefault(config.getToken())
|
return JDABuilder.createDefault(config.getToken())
|
||||||
.setStatus(OnlineStatus.ONLINE)
|
.setStatus(OnlineStatus.ONLINE)
|
||||||
|
|
@ -166,85 +74,12 @@ public class BotInitializer {
|
||||||
GatewayIntent.GUILD_MEMBERS,
|
GatewayIntent.GUILD_MEMBERS,
|
||||||
GatewayIntent.GUILD_VOICE_STATES
|
GatewayIntent.GUILD_VOICE_STATES
|
||||||
)
|
)
|
||||||
.addEventListeners(
|
.addEventListeners(listenerAggregator.getAll())
|
||||||
new ReadyListener(),
|
|
||||||
new MessageListener(dispatcher, dataStore, levelSystem),
|
|
||||||
new SlashCommandListener(slashRegistry, rateLimiter, metrics),
|
|
||||||
voiceTracker,
|
|
||||||
voiceChannelListener,
|
|
||||||
memberCounterListener,
|
|
||||||
voiceSetupCommand,
|
|
||||||
counterSetupCommand,
|
|
||||||
logSetupCommand,
|
|
||||||
voiceButtonHandler,
|
|
||||||
moderationLogger,
|
|
||||||
helpMenuHandler,
|
|
||||||
new LeaderboardButtonListener(levelSystem)
|
|
||||||
)
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerSlashCommandsInDiscord(JDA jda) {
|
private void registerShutdownHook(JDA jda) {
|
||||||
try {
|
Runtime.getRuntime().addShutdownHook(new Thread(
|
||||||
jda.awaitReady();
|
() -> shutdownManager.shutdown(jda), "ShutdownHook"));
|
||||||
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();
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,130 +1,130 @@
|
||||||
package creator.loki.core;
|
package creator.loki.core;
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
import creator.loki.core.registry.CommandRegistry;
|
import creator.loki.core.registry.CommandRegistry;
|
||||||
import creator.loki.embed.EmbedTemplates;
|
import creator.loki.embed.EmbedTemplates;
|
||||||
import creator.loki.exception.CommandExceptionHandler;
|
import creator.loki.core.exception.CommandExceptionHandler;
|
||||||
import creator.loki.metrics.MetricsCollector;
|
import creator.loki.feature.metrics.MetricsCollector;
|
||||||
import creator.loki.core.security.RateLimiter;
|
import creator.loki.core.security.RateLimiter;
|
||||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class EventDispatcher {
|
public class EventDispatcher {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(EventDispatcher.class);
|
private static final Logger logger = LoggerFactory.getLogger(EventDispatcher.class);
|
||||||
private final BotConfig config;
|
private final BotConfig config;
|
||||||
private final CommandRegistry registry;
|
private final CommandRegistry registry;
|
||||||
private final ExecutorService executorService;
|
private final ExecutorService executorService;
|
||||||
private final RateLimiter rateLimiter;
|
private final RateLimiter rateLimiter;
|
||||||
private final MetricsCollector metrics;
|
private final MetricsCollector metrics;
|
||||||
|
|
||||||
public EventDispatcher(BotConfig config, CommandRegistry registry, RateLimiter rateLimiter, MetricsCollector metrics) {
|
public EventDispatcher(BotConfig config, CommandRegistry registry, RateLimiter rateLimiter, MetricsCollector metrics) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
this.executorService = Executors.newCachedThreadPool();
|
this.executorService = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
this.rateLimiter = rateLimiter;
|
this.rateLimiter = rateLimiter;
|
||||||
this.metrics = metrics;
|
this.metrics = metrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleMessage(MessageReceivedEvent event) {
|
public void handleMessage(MessageReceivedEvent event) {
|
||||||
// Игнорируем ботов
|
// Игнорируем ботов
|
||||||
if (event.getAuthor().isBot()) {
|
if (event.getAuthor().isBot()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String content = event.getMessage().getContentRaw();
|
String content = event.getMessage().getContentRaw();
|
||||||
if (!content.startsWith(config.getPrefix())) {
|
if (!content.startsWith(config.getPrefix())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Парсим команду и аргументы
|
// Парсим команду и аргументы
|
||||||
String[] parts = content.substring(config.getPrefix().length()).trim().split("\\s+");
|
String[] parts = content.substring(config.getPrefix().length()).trim().split("\\s+");
|
||||||
if (parts.length == 0) {
|
if (parts.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String commandName = parts[0].toLowerCase();
|
String commandName = parts[0].toLowerCase();
|
||||||
List<String> args = parts.length > 1
|
List<String> args = parts.length > 1
|
||||||
? Arrays.asList(Arrays.copyOfRange(parts, 1, parts.length))
|
? Arrays.asList(Arrays.copyOfRange(parts, 1, parts.length))
|
||||||
: List.of();
|
: List.of();
|
||||||
|
|
||||||
Command command = registry.getCommand(commandName);
|
Command command = registry.getCommand(commandName);
|
||||||
if (command == null) {
|
if (command == null) {
|
||||||
event.getChannel().sendMessageEmbeds(
|
event.getChannel().sendMessageEmbeds(
|
||||||
EmbedTemplates.error(
|
EmbedTemplates.error(
|
||||||
"Команда не найдена",
|
"Команда не найдена",
|
||||||
"Команда `" + commandName + "` не существует. Используй `" + config.getPrefix() + "help`"
|
"Команда `" + commandName + "` не существует. Используй `" + config.getPrefix() + "help`"
|
||||||
)
|
)
|
||||||
).queue();
|
).queue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем глобальный rate limit
|
// Проверяем глобальный rate limit
|
||||||
String userId = event.getAuthor().getId();
|
String userId = event.getAuthor().getId();
|
||||||
String guildId = event.getGuild() != null ? event.getGuild().getId() : "DM";
|
String guildId = event.getGuild() != null ? event.getGuild().getId() : "DM";
|
||||||
|
|
||||||
if (!rateLimiter.allow(userId, guildId)) {
|
if (!rateLimiter.allow(userId, guildId)) {
|
||||||
long waitTime = rateLimiter.getUserWaitTime(userId);
|
long waitTime = rateLimiter.getUserWaitTime(userId);
|
||||||
event.getChannel().sendMessageEmbeds(
|
event.getChannel().sendMessageEmbeds(
|
||||||
EmbedTemplates.error(
|
EmbedTemplates.error(
|
||||||
"⏱️ Слишком быстро!",
|
"⏱️ Слишком быстро!",
|
||||||
"Ты отправляешь команды слишком часто. Подожди " + waitTime + " секунд."
|
"Ты отправляешь команды слишком часто. Подожди " + waitTime + " секунд."
|
||||||
)
|
)
|
||||||
).queue();
|
).queue();
|
||||||
logger.warn("Rate limit для пользователя {} в гильдии {}", userId, guildId);
|
logger.warn("Rate limit для пользователя {} в гильдии {}", userId, guildId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Выполняем команду асинхронно
|
// Выполняем команду асинхронно
|
||||||
executorService.submit(() -> {
|
executorService.submit(() -> {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
try {
|
try {
|
||||||
CommandContext context = new CommandContext(
|
CommandContext context = new CommandContext(
|
||||||
event.getMessage(),
|
event.getMessage(),
|
||||||
args,
|
args,
|
||||||
event.getChannel(),
|
event.getChannel(),
|
||||||
event.getMember(),
|
event.getMember(),
|
||||||
event.getGuild()
|
event.getGuild()
|
||||||
);
|
);
|
||||||
command.execute(context);
|
command.execute(context);
|
||||||
|
|
||||||
// Записываем метрики
|
// Записываем метрики
|
||||||
long executionTime = System.currentTimeMillis() - startTime;
|
long executionTime = System.currentTimeMillis() - startTime;
|
||||||
metrics.recordCommand(commandName, executionTime);
|
metrics.recordCommand(commandName, executionTime);
|
||||||
|
|
||||||
logger.info("Выполнена команда: {} от {} за {} мс",
|
logger.info("Выполнена команда: {} от {} за {} мс",
|
||||||
commandName, event.getAuthor().getName(), executionTime);
|
commandName, event.getAuthor().getName(), executionTime);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Записываем ошибку в метрики
|
// Записываем ошибку в метрики
|
||||||
metrics.recordError(commandName);
|
metrics.recordError(commandName);
|
||||||
|
|
||||||
// Используем централизованный обработчик ошибок
|
// Используем централизованный обработчик ошибок
|
||||||
CommandExceptionHandler.handle(e, event.getChannel(), commandName);
|
CommandExceptionHandler.handle(e, event.getChannel(), commandName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
logger.info("Завершение работы EventDispatcher...");
|
logger.info("Завершение работы EventDispatcher...");
|
||||||
executorService.shutdown();
|
executorService.shutdown();
|
||||||
try {
|
try {
|
||||||
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
|
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||||
logger.warn("ExecutorService не завершился за 10 секунд, принудительная остановка");
|
logger.warn("ExecutorService не завершился за 10 секунд, принудительная остановка");
|
||||||
executorService.shutdownNow();
|
executorService.shutdownNow();
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
logger.error("Прервано ожидание завершения ExecutorService", e);
|
logger.error("Прервано ожидание завершения ExecutorService", e);
|
||||||
executorService.shutdownNow();
|
executorService.shutdownNow();
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
logger.info("EventDispatcher завершён");
|
logger.info("EventDispatcher завершён");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
54
src/main/java/creator/loki/core/ListenerAggregator.java
Normal file
54
src/main/java/creator/loki/core/ListenerAggregator.java
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package creator.loki.core;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import creator.loki.feature.level.MessageListener;
|
||||||
|
import creator.loki.feature.moderation.ModerationLogger;
|
||||||
|
import creator.loki.bootstrap.ReadyListener;
|
||||||
|
import creator.loki.feature.help.HelpMenuHandler;
|
||||||
|
import creator.loki.feature.counter.MemberCounterListener;
|
||||||
|
import creator.loki.feature.leaderboard.LeaderboardButtonListener;
|
||||||
|
import creator.loki.feature.level.LevelUpNotifier;
|
||||||
|
import creator.loki.feature.level.ResetLevelsButtonHandler;
|
||||||
|
import creator.loki.feature.level.VoiceTracker;
|
||||||
|
import creator.loki.feature.voice.VoiceButtonHandler;
|
||||||
|
import creator.loki.feature.voice.VoiceChannelListener;
|
||||||
|
import creator.loki.feature.voice.VoiceModalHandler;
|
||||||
|
import creator.loki.feature.voice.VoiceSyncListener;
|
||||||
|
import creator.loki.feature.welcome.WelcomeListener;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class ListenerAggregator {
|
||||||
|
private final Object[] listeners;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ListenerAggregator(
|
||||||
|
ReadyListener readyListener,
|
||||||
|
MessageListener messageListener,
|
||||||
|
VoiceTracker voiceTracker,
|
||||||
|
VoiceChannelListener voiceChannelListener,
|
||||||
|
MemberCounterListener memberCounterListener,
|
||||||
|
VoiceButtonHandler voiceButtonHandler,
|
||||||
|
VoiceModalHandler voiceModalHandler,
|
||||||
|
VoiceSyncListener voiceSyncListener,
|
||||||
|
WelcomeListener welcomeListener,
|
||||||
|
ModerationLogger moderationLogger,
|
||||||
|
HelpMenuHandler helpMenuHandler,
|
||||||
|
ResetLevelsButtonHandler resetLevelsButtonHandler,
|
||||||
|
LeaderboardButtonListener leaderboardButtonListener,
|
||||||
|
MessageCache messageCache
|
||||||
|
) {
|
||||||
|
this.listeners = new Object[]{
|
||||||
|
readyListener, messageListener, voiceTracker,
|
||||||
|
voiceChannelListener, memberCounterListener,
|
||||||
|
voiceButtonHandler, voiceModalHandler, voiceSyncListener,
|
||||||
|
welcomeListener, moderationLogger, helpMenuHandler,
|
||||||
|
resetLevelsButtonHandler, leaderboardButtonListener,
|
||||||
|
messageCache
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object[] getAll() {
|
||||||
|
return listeners;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/main/java/creator/loki/core/MessageCache.java
Normal file
32
src/main/java/creator/loki/core/MessageCache.java
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package creator.loki.core;
|
||||||
|
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||||
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class MessageCache extends ListenerAdapter {
|
||||||
|
private final Cache<String, String> cache;
|
||||||
|
|
||||||
|
public MessageCache() {
|
||||||
|
this.cache = Caffeine.newBuilder()
|
||||||
|
.maximumSize(10_000)
|
||||||
|
.expireAfterWrite(1, TimeUnit.HOURS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessageReceived(MessageReceivedEvent event) {
|
||||||
|
if (event.getAuthor().isBot()) return;
|
||||||
|
String content = event.getMessage().getContentRaw();
|
||||||
|
if (!content.isBlank()) {
|
||||||
|
cache.put(event.getMessageId(), content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContent(String messageId) {
|
||||||
|
return cache.getIfPresent(messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/main/java/creator/loki/core/ShutdownManager.java
Normal file
85
src/main/java/creator/loki/core/ShutdownManager.java
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package creator.loki.core;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import creator.loki.database.Database;
|
||||||
|
import creator.loki.feature.counter.MemberCounterListener;
|
||||||
|
import creator.loki.feature.level.VoiceTracker;
|
||||||
|
import creator.loki.feature.moderation.MuteScheduler;
|
||||||
|
import creator.loki.feature.metrics.HealthHttpServer;
|
||||||
|
import creator.loki.core.security.CooldownManager;
|
||||||
|
import creator.loki.core.security.RateLimiter;
|
||||||
|
import net.dv8tion.jda.api.JDA;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class ShutdownManager {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ShutdownManager.class);
|
||||||
|
|
||||||
|
private final HealthHttpServer healthHttpServer;
|
||||||
|
private final CooldownManager cooldownManager;
|
||||||
|
private final RateLimiter rateLimiter;
|
||||||
|
private final VoiceTracker voiceTracker;
|
||||||
|
private final MemberCounterListener memberCounterListener;
|
||||||
|
private final MuteScheduler muteScheduler;
|
||||||
|
private final EventDispatcher dispatcher;
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ShutdownManager(
|
||||||
|
HealthHttpServer healthHttpServer,
|
||||||
|
CooldownManager cooldownManager,
|
||||||
|
RateLimiter rateLimiter,
|
||||||
|
VoiceTracker voiceTracker,
|
||||||
|
MemberCounterListener memberCounterListener,
|
||||||
|
MuteScheduler muteScheduler,
|
||||||
|
EventDispatcher dispatcher,
|
||||||
|
Database database
|
||||||
|
) {
|
||||||
|
this.healthHttpServer = healthHttpServer;
|
||||||
|
this.cooldownManager = cooldownManager;
|
||||||
|
this.rateLimiter = rateLimiter;
|
||||||
|
this.voiceTracker = voiceTracker;
|
||||||
|
this.memberCounterListener = memberCounterListener;
|
||||||
|
this.muteScheduler = muteScheduler;
|
||||||
|
this.dispatcher = dispatcher;
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown(JDA jda) {
|
||||||
|
logger.info("========================================");
|
||||||
|
logger.info("Получен сигнал завершения работы бота...");
|
||||||
|
logger.info("========================================");
|
||||||
|
|
||||||
|
try {
|
||||||
|
healthHttpServer.stop();
|
||||||
|
logger.info("Остановка менеджеров...");
|
||||||
|
cooldownManager.shutdown();
|
||||||
|
rateLimiter.shutdown();
|
||||||
|
voiceTracker.shutdown();
|
||||||
|
memberCounterListener.shutdown();
|
||||||
|
muteScheduler.shutdown();
|
||||||
|
dispatcher.shutdown();
|
||||||
|
|
||||||
|
logger.info("Закрытие базы данных...");
|
||||||
|
database.close();
|
||||||
|
|
||||||
|
logger.info("Отключение JDA...");
|
||||||
|
jda.shutdown();
|
||||||
|
|
||||||
|
if (!jda.awaitShutdown(Duration.ofSeconds(10))) {
|
||||||
|
logger.warn("JDA не завершился за 10 секунд, принудительная остановка");
|
||||||
|
jda.shutdownNow();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Ошибка при завершении работы бота", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("========================================");
|
||||||
|
logger.info("Бот успешно завершён!");
|
||||||
|
logger.info("========================================");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
package creator.loki.exception;
|
package creator.loki.core.exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Исключение для ошибок выполнения команд
|
* Исключение для ошибок выполнения команд
|
||||||
*/
|
*/
|
||||||
public class CommandException extends Exception {
|
public class CommandException extends Exception {
|
||||||
private final String userMessage;
|
private static final long serialVersionUID = 1L;
|
||||||
|
private final String userMessage;
|
||||||
public CommandException(String userMessage) {
|
|
||||||
super(userMessage);
|
public CommandException(String userMessage) {
|
||||||
this.userMessage = userMessage;
|
super(userMessage);
|
||||||
}
|
this.userMessage = userMessage;
|
||||||
|
}
|
||||||
public CommandException(String userMessage, Throwable cause) {
|
|
||||||
super(userMessage, cause);
|
public CommandException(String userMessage, Throwable cause) {
|
||||||
this.userMessage = userMessage;
|
super(userMessage, cause);
|
||||||
}
|
this.userMessage = userMessage;
|
||||||
|
}
|
||||||
public String getUserMessage() {
|
|
||||||
return userMessage;
|
public String getUserMessage() {
|
||||||
}
|
return userMessage;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,63 +1,63 @@
|
||||||
package creator.loki.exception;
|
package creator.loki.core.exception;
|
||||||
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
import creator.loki.embed.EmbedFactory;
|
||||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обработчик исключений для команд
|
* Обработчик исключений для команд
|
||||||
*/
|
*/
|
||||||
public class CommandExceptionHandler {
|
public class CommandExceptionHandler {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CommandExceptionHandler.class);
|
private static final Logger logger = LoggerFactory.getLogger(CommandExceptionHandler.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обрабатывает исключение и отправляет сообщение пользователю
|
* Обрабатывает исключение и отправляет сообщение пользователю
|
||||||
*/
|
*/
|
||||||
public static void handle(Throwable throwable, MessageChannel channel, String commandName) {
|
public static void handle(Throwable throwable, MessageChannel channel, String commandName) {
|
||||||
logger.error("Ошибка при выполнении команды: {}", commandName, throwable);
|
logger.error("Ошибка при выполнении команды: {}", commandName, throwable);
|
||||||
|
|
||||||
String userMessage;
|
String userMessage;
|
||||||
if (throwable instanceof CommandException) {
|
if (throwable instanceof CommandException ce) {
|
||||||
userMessage = ((CommandException) throwable).getUserMessage();
|
userMessage = ce.getUserMessage();
|
||||||
} else {
|
} else {
|
||||||
userMessage = "Произошла непредвиденная ошибка при выполнении команды.";
|
userMessage = "Не удалось выполнить команду. Попробуйте позже или обратитесь к администратору.";
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageEmbed errorEmbed = EmbedFactory.create()
|
MessageEmbed errorEmbed = EmbedFactory.create()
|
||||||
.setTitle("❌ Ошибка")
|
.withTitle("❌ Ошибка")
|
||||||
.setDescription(userMessage)
|
.withDescription(userMessage)
|
||||||
.setColor(Color.RED)
|
.withColor(Color.RED)
|
||||||
.addField("Команда", commandName, true)
|
.addField("Команда", commandName, true)
|
||||||
.setFooter("Если ошибка повторяется, обратитесь к администратору")
|
.withFooter("Если ошибка повторяется, обратитесь к администратору")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
channel.sendMessageEmbeds(errorEmbed).queue(
|
channel.sendMessageEmbeds(errorEmbed).queue(
|
||||||
success -> {},
|
success -> {},
|
||||||
error -> logger.error("Не удалось отправить сообщение об ошибке", error)
|
error -> logger.error("Не удалось отправить сообщение об ошибке", error)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обрабатывает исключение с кастомным сообщением
|
* Обрабатывает исключение с кастомным сообщением
|
||||||
*/
|
*/
|
||||||
public static void handle(Throwable throwable, MessageChannel channel, String commandName, String customMessage) {
|
public static void handle(Throwable throwable, MessageChannel channel, String commandName, String customMessage) {
|
||||||
logger.error("Ошибка при выполнении команды: {}", commandName, throwable);
|
logger.error("Ошибка при выполнении команды: {}", commandName, throwable);
|
||||||
|
|
||||||
MessageEmbed errorEmbed = EmbedFactory.create()
|
MessageEmbed errorEmbed = EmbedFactory.create()
|
||||||
.setTitle("❌ Ошибка")
|
.withTitle("❌ Ошибка")
|
||||||
.setDescription(customMessage)
|
.withDescription(customMessage)
|
||||||
.setColor(Color.RED)
|
.withColor(Color.RED)
|
||||||
.addField("Команда", commandName, true)
|
.addField("Команда", commandName, true)
|
||||||
.setFooter("Если ошибка повторяется, обратитесь к администратору")
|
.withFooter("Если ошибка повторяется, обратитесь к администратору")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
channel.sendMessageEmbeds(errorEmbed).queue(
|
channel.sendMessageEmbeds(errorEmbed).queue(
|
||||||
success -> {},
|
success -> {},
|
||||||
error -> logger.error("Не удалось отправить сообщение об ошибке", error)
|
error -> logger.error("Не удалось отправить сообщение об ошибке", error)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package creator.loki.core.registry;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface CommandRegistrar {
|
||||||
|
void register(CommandRegistry registry);
|
||||||
|
}
|
||||||
|
|
@ -1,32 +1,38 @@
|
||||||
package creator.loki.core.registry;
|
package creator.loki.core.registry;
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class CommandRegistry {
|
public class CommandRegistry {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CommandRegistry.class);
|
private static final Logger logger = LoggerFactory.getLogger(CommandRegistry.class);
|
||||||
private final Map<String, Command> commands = new HashMap<>();
|
private final Map<String, Command> commands = new HashMap<>();
|
||||||
|
|
||||||
public void register(Command command) {
|
public void register(Command command) {
|
||||||
commands.put(command.getName().toLowerCase(), command);
|
String key = command.getName().toLowerCase();
|
||||||
logger.debug("Зарегистрирована команда: {}", command.getName());
|
Command previous = commands.put(key, command);
|
||||||
}
|
if (previous != null) {
|
||||||
|
logger.warn("Имя команды «{}» уже занято: {} заменена на {}",
|
||||||
public Command getCommand(String name) {
|
key, previous.getClass().getSimpleName(), command.getClass().getSimpleName());
|
||||||
return commands.get(name.toLowerCase());
|
} else {
|
||||||
}
|
logger.debug("Зарегистрирована команда: {}", command.getName());
|
||||||
|
}
|
||||||
public Collection<Command> getAllCommands() {
|
}
|
||||||
return Collections.unmodifiableCollection(commands.values());
|
|
||||||
}
|
public Command getCommand(String name) {
|
||||||
|
return commands.get(name.toLowerCase());
|
||||||
public int getCommandCount() {
|
}
|
||||||
return commands.size();
|
|
||||||
}
|
public Collection<Command> getAllCommands() {
|
||||||
}
|
return Collections.unmodifiableCollection(commands.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCommandCount() {
|
||||||
|
return commands.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,156 +1,29 @@
|
||||||
package creator.loki.core.registry;
|
package creator.loki.core.registry;
|
||||||
|
|
||||||
import creator.loki.command.impl.entertainment.games.*;
|
import com.google.inject.Inject;
|
||||||
import creator.loki.command.impl.entertainment.games.random.CoinCommand;
|
import com.google.inject.Singleton;
|
||||||
import creator.loki.command.impl.entertainment.games.random.DiceCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.games.random.GlazbobaCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.games.random.MemeCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.games.random.RateCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.games.rating.BoykisserCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.games.rating.FatCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.games.rating.GayCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.games.rating.IqCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.games.rating.MustacheCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.games.rating.PpCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.interaction.*;
|
|
||||||
import creator.loki.command.impl.entertainment.interaction.negative.PokeCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.interaction.negative.SlapCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.interaction.negative.TickleCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.interaction.positive.CuddleCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.interaction.positive.FeedCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.interaction.positive.HugCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.interaction.positive.KissCommand;
|
|
||||||
import creator.loki.command.impl.entertainment.interaction.positive.PatCommand;
|
|
||||||
import creator.loki.command.impl.moderation.*;
|
|
||||||
import creator.loki.feature.role.*;
|
|
||||||
import creator.loki.command.impl.stats.*;
|
|
||||||
import creator.loki.command.impl.utility.*;
|
|
||||||
import creator.loki.command.impl.utility.info.*;
|
|
||||||
import creator.loki.command.impl.utility.system.*;
|
|
||||||
import creator.loki.config.BotConfig;
|
|
||||||
import creator.loki.data.JsonDataStore;
|
|
||||||
import creator.loki.feature.level.service.LevelService;
|
|
||||||
import creator.loki.feature.role.CreateRoleCommand;
|
|
||||||
import creator.loki.feature.role.DeleteRoleCommand;
|
|
||||||
import creator.loki.feature.role.EditRoleCommand;
|
|
||||||
import creator.loki.feature.role.MyRolesCommand;
|
|
||||||
import creator.loki.level.LevelSystem;
|
|
||||||
import creator.loki.metrics.MetricsCollector;
|
|
||||||
import creator.loki.profile.ProfileService;
|
|
||||||
import creator.loki.feature.role.CustomRoleManager;
|
|
||||||
import creator.loki.core.security.CooldownManager;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
import java.util.Set;
|
||||||
* Регистратор префиксных команд (!) - Single Responsibility
|
|
||||||
*/
|
@Singleton
|
||||||
public class PrefixCommandRegistrar {
|
public class PrefixCommandRegistrar {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(PrefixCommandRegistrar.class);
|
private static final Logger logger = LoggerFactory.getLogger(PrefixCommandRegistrar.class);
|
||||||
|
|
||||||
private final CommandRegistry registry;
|
private final CommandRegistry registry;
|
||||||
private final BotConfig config;
|
private final Set<CommandRegistrar> registrars;
|
||||||
private final JsonDataStore dataStore;
|
|
||||||
private final CustomRoleManager roleManager;
|
|
||||||
private final CooldownManager cooldownManager;
|
|
||||||
private final LevelSystem levelSystem;
|
|
||||||
private final LevelService levelService;
|
|
||||||
private final creator.loki.feature.profile.ProfileService newProfileService;
|
|
||||||
private final MetricsCollector metrics;
|
|
||||||
|
|
||||||
public PrefixCommandRegistrar(
|
@Inject
|
||||||
CommandRegistry registry,
|
public PrefixCommandRegistrar(CommandRegistry registry, Set<CommandRegistrar> registrars) {
|
||||||
BotConfig config,
|
|
||||||
JsonDataStore dataStore,
|
|
||||||
CustomRoleManager roleManager,
|
|
||||||
CooldownManager cooldownManager,
|
|
||||||
LevelSystem levelSystem,
|
|
||||||
LevelService levelService,
|
|
||||||
creator.loki.feature.profile.ProfileService newProfileService,
|
|
||||||
MetricsCollector metrics
|
|
||||||
) {
|
|
||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
this.config = config;
|
this.registrars = registrars;
|
||||||
this.dataStore = dataStore;
|
|
||||||
this.roleManager = roleManager;
|
|
||||||
this.cooldownManager = cooldownManager;
|
|
||||||
this.levelSystem = levelSystem;
|
|
||||||
this.levelService = levelService;
|
|
||||||
this.newProfileService = newProfileService;
|
|
||||||
this.metrics = metrics;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerAll() {
|
public void registerAll() {
|
||||||
registerUtilityCommands();
|
for (CommandRegistrar registrar : registrars) {
|
||||||
registerRoleCommands();
|
registrar.register(registry);
|
||||||
registerInteractionCommands();
|
}
|
||||||
registerGameCommands();
|
|
||||||
registerStatsCommands();
|
|
||||||
registerModerationCommands();
|
|
||||||
|
|
||||||
logger.info("Зарегистрировано префиксных команд: {}", registry.getCommandCount());
|
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 creator.loki.feature.profile.ProfileCommand(newProfileService));
|
|
||||||
registry.register(new MetricsCommand(metrics));
|
|
||||||
registry.register(new AvatarCommand());
|
|
||||||
registry.register(new PollCommand());
|
|
||||||
registry.register(new SwapCommand());
|
|
||||||
}
|
|
||||||
|
|
||||||
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 creator.loki.feature.level.RankCommand(levelService));
|
|
||||||
registry.register(new creator.loki.command.impl.stats.leaderboard.NewLeaderboardCommand(levelSystem));
|
|
||||||
registry.register(new RepCommand(dataStore, cooldownManager));
|
|
||||||
registry.register(new MinusRepCommand(dataStore, cooldownManager));
|
|
||||||
registry.register(new RepTopCommand(dataStore));
|
|
||||||
registry.register(new creator.loki.feature.level.StatsCommand());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerModerationCommands() {
|
|
||||||
registry.register(new BanCommand());
|
|
||||||
registry.register(new KickCommand());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
package creator.loki.core.registry;
|
|
||||||
|
|
||||||
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 SlashCommandRegistry registry;
|
|
||||||
private final JsonDataStore dataStore;
|
|
||||||
private final MetricsCollector metrics;
|
|
||||||
|
|
||||||
public SlashCommandRegistrar(
|
|
||||||
SlashCommandRegistry registry,
|
|
||||||
JsonDataStore dataStore,
|
|
||||||
MetricsCollector metrics
|
|
||||||
) {
|
|
||||||
this.registry = registry;
|
|
||||||
this.dataStore = dataStore;
|
|
||||||
this.metrics = metrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void registerAll() {
|
|
||||||
// Все slash команды удалены согласно рефакторингу
|
|
||||||
// Используются только prefix команды (!)
|
|
||||||
|
|
||||||
logger.info("Зарегистрировано slash команд: {}", registry.getCommandCount());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
package creator.loki.core.registry;
|
|
||||||
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,83 +1,83 @@
|
||||||
package creator.loki.core.security;
|
package creator.loki.core.security;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class CooldownManager {
|
public class CooldownManager {
|
||||||
private final ConcurrentHashMap<String, Instant> cooldowns = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, Instant> cooldowns = new ConcurrentHashMap<>();
|
||||||
private final ScheduledExecutorService cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
|
private final ScheduledExecutorService cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
public CooldownManager() {
|
@SuppressWarnings("this-escape")
|
||||||
// Очистка истёкших cooldown'ов каждые 5 минут
|
public CooldownManager() {
|
||||||
cleanupExecutor.scheduleAtFixedRate(this::cleanup, 5, 5, TimeUnit.MINUTES);
|
cleanupExecutor.scheduleAtFixedRate(() -> Thread.startVirtualThread(this::cleanup), 5, 5, TimeUnit.MINUTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет, находится ли пользователь на cooldown для команды
|
* Проверяет, находится ли пользователь на cooldown для команды
|
||||||
*/
|
*/
|
||||||
public boolean isOnCooldown(String userId, String commandName) {
|
public boolean isOnCooldown(String userId, String commandName) {
|
||||||
String key = userId + ":" + commandName;
|
String key = userId + ":" + commandName;
|
||||||
Instant cooldownEnd = cooldowns.get(key);
|
Instant cooldownEnd = cooldowns.get(key);
|
||||||
|
|
||||||
if (cooldownEnd == null) {
|
if (cooldownEnd == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Instant.now().isAfter(cooldownEnd)) {
|
if (Instant.now().isAfter(cooldownEnd)) {
|
||||||
cooldowns.remove(key);
|
cooldowns.remove(key);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Возвращает оставшееся время cooldown в секундах
|
* Возвращает оставшееся время cooldown в секундах
|
||||||
*/
|
*/
|
||||||
public long getRemainingSeconds(String userId, String commandName) {
|
public long getRemainingSeconds(String userId, String commandName) {
|
||||||
String key = userId + ":" + commandName;
|
String key = userId + ":" + commandName;
|
||||||
Instant cooldownEnd = cooldowns.get(key);
|
Instant cooldownEnd = cooldowns.get(key);
|
||||||
|
|
||||||
if (cooldownEnd == null) {
|
if (cooldownEnd == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
long remaining = cooldownEnd.getEpochSecond() - Instant.now().getEpochSecond();
|
long remaining = cooldownEnd.getEpochSecond() - Instant.now().getEpochSecond();
|
||||||
return Math.max(0, remaining);
|
return Math.max(0, remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Применяет cooldown для пользователя на команду
|
* Применяет cooldown для пользователя на команду
|
||||||
*/
|
*/
|
||||||
public void applyCooldown(String userId, String commandName, int seconds) {
|
public void applyCooldown(String userId, String commandName, int seconds) {
|
||||||
String key = userId + ":" + commandName;
|
String key = userId + ":" + commandName;
|
||||||
Instant cooldownEnd = Instant.now().plusSeconds(seconds);
|
Instant cooldownEnd = Instant.now().plusSeconds(seconds);
|
||||||
cooldowns.put(key, cooldownEnd);
|
cooldowns.put(key, cooldownEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Очищает истёкшие cooldown'ы
|
* Очищает истёкшие cooldown'ы
|
||||||
*/
|
*/
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
cooldowns.entrySet().removeIf(entry -> now.isAfter(entry.getValue()));
|
cooldowns.entrySet().removeIf(entry -> now.isAfter(entry.getValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Завершает работу менеджера
|
* Завершает работу менеджера
|
||||||
*/
|
*/
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
cleanupExecutor.shutdown();
|
cleanupExecutor.shutdown();
|
||||||
try {
|
try {
|
||||||
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
cleanupExecutor.shutdownNow();
|
cleanupExecutor.shutdownNow();
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
cleanupExecutor.shutdownNow();
|
cleanupExecutor.shutdownNow();
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,157 +1,163 @@
|
||||||
package creator.loki.core.security;
|
package creator.loki.core.security;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rate limiter для защиты от спама
|
* Rate limiter для защиты от спама
|
||||||
* Использует алгоритм Token Bucket
|
* Использует алгоритм Token Bucket
|
||||||
*/
|
*/
|
||||||
public class RateLimiter {
|
public class RateLimiter {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(RateLimiter.class);
|
private static final Logger logger = LoggerFactory.getLogger(RateLimiter.class);
|
||||||
|
|
||||||
// Лимиты по умолчанию
|
// Лимиты по умолчанию
|
||||||
private static final int DEFAULT_USER_CAPACITY = 10; // Максимум токенов на пользователя
|
private static final int DEFAULT_USER_CAPACITY = 10; // Максимум токенов на пользователя
|
||||||
private static final int DEFAULT_USER_REFILL_RATE = 2; // Токенов в секунду
|
private static final int DEFAULT_USER_REFILL_RATE = 2; // Токенов в секунду
|
||||||
private static final int DEFAULT_GUILD_CAPACITY = 50; // Максимум токенов на гильдию
|
private static final int DEFAULT_GUILD_CAPACITY = 50; // Максимум токенов на гильдию
|
||||||
private static final int DEFAULT_GUILD_REFILL_RATE = 10; // Токенов в секунду
|
private static final int DEFAULT_GUILD_REFILL_RATE = 10; // Токенов в секунду
|
||||||
|
|
||||||
private final ConcurrentHashMap<String, TokenBucket> userBuckets = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, TokenBucket> userBuckets = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<String, TokenBucket> guildBuckets = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, TokenBucket> guildBuckets = new ConcurrentHashMap<>();
|
||||||
private final ScheduledExecutorService cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
|
private final ScheduledExecutorService cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
public RateLimiter() {
|
public RateLimiter() {
|
||||||
// Очистка старых bucket'ов каждые 5 минут
|
// Очистка старых bucket'ов каждые 5 минут
|
||||||
cleanupExecutor.scheduleAtFixedRate(this::cleanup, 5, 5, TimeUnit.MINUTES);
|
cleanupExecutor.scheduleAtFixedRate(() -> Thread.startVirtualThread(this::cleanup), 5, 5, TimeUnit.MINUTES);
|
||||||
logger.info("RateLimiter инициализирован");
|
logger.info("RateLimiter инициализирован");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет, может ли пользователь выполнить действие
|
* Проверяет, может ли пользователь выполнить действие
|
||||||
*/
|
*/
|
||||||
public boolean allowUser(String userId) {
|
public boolean allowUser(String userId) {
|
||||||
TokenBucket bucket = userBuckets.computeIfAbsent(
|
TokenBucket bucket = userBuckets.computeIfAbsent(
|
||||||
userId,
|
userId,
|
||||||
k -> new TokenBucket(DEFAULT_USER_CAPACITY, DEFAULT_USER_REFILL_RATE)
|
k -> new TokenBucket(DEFAULT_USER_CAPACITY, DEFAULT_USER_REFILL_RATE)
|
||||||
);
|
);
|
||||||
return bucket.tryConsume();
|
return bucket.tryConsume();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет, может ли гильдия выполнить действие
|
* Проверяет, может ли гильдия выполнить действие
|
||||||
*/
|
*/
|
||||||
public boolean allowGuild(String guildId) {
|
public boolean allowGuild(String guildId) {
|
||||||
TokenBucket bucket = guildBuckets.computeIfAbsent(
|
TokenBucket bucket = guildBuckets.computeIfAbsent(
|
||||||
guildId,
|
guildId,
|
||||||
k -> new TokenBucket(DEFAULT_GUILD_CAPACITY, DEFAULT_GUILD_REFILL_RATE)
|
k -> new TokenBucket(DEFAULT_GUILD_CAPACITY, DEFAULT_GUILD_REFILL_RATE)
|
||||||
);
|
);
|
||||||
return bucket.tryConsume();
|
return bucket.tryConsume();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет оба лимита: пользователя и гильдии
|
* Проверяет оба лимита: пользователя и гильдии
|
||||||
*/
|
*/
|
||||||
public boolean allow(String userId, String guildId) {
|
public boolean allow(String userId, String guildId) {
|
||||||
return allowUser(userId) && allowGuild(guildId);
|
return allowUser(userId) && allowGuild(guildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Возвращает оставшееся время до восстановления для пользователя (в секундах)
|
* Возвращает оставшееся время до восстановления для пользователя (в секундах)
|
||||||
*/
|
*/
|
||||||
public long getUserWaitTime(String userId) {
|
public long getUserWaitTime(String userId) {
|
||||||
TokenBucket bucket = userBuckets.get(userId);
|
TokenBucket bucket = userBuckets.get(userId);
|
||||||
if (bucket == null) {
|
if (bucket == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return bucket.getWaitTime();
|
return bucket.getWaitTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Очищает неактивные bucket'ы
|
* Очищает неактивные bucket'ы
|
||||||
*/
|
*/
|
||||||
private void cleanup() {
|
private void cleanup() {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
long threshold = now - TimeUnit.MINUTES.toMillis(10);
|
long threshold = now - TimeUnit.MINUTES.toMillis(10);
|
||||||
|
|
||||||
userBuckets.entrySet().removeIf(entry -> entry.getValue().getLastAccessTime() < threshold);
|
int usersBefore = userBuckets.size();
|
||||||
guildBuckets.entrySet().removeIf(entry -> entry.getValue().getLastAccessTime() < threshold);
|
int guildsBefore = guildBuckets.size();
|
||||||
|
userBuckets.entrySet().removeIf(entry -> entry.getValue().getLastAccessTime() < threshold);
|
||||||
logger.debug("Очистка rate limiter: осталось {} user buckets, {} guild buckets",
|
guildBuckets.entrySet().removeIf(entry -> entry.getValue().getLastAccessTime() < threshold);
|
||||||
userBuckets.size(), guildBuckets.size());
|
|
||||||
}
|
int removedUsers = usersBefore - userBuckets.size();
|
||||||
|
int removedGuilds = guildsBefore - guildBuckets.size();
|
||||||
/**
|
if (removedUsers > 0 || removedGuilds > 0) {
|
||||||
* Завершает работу rate limiter
|
logger.debug("Очистка rate limiter: удалено {} user / {} guild buckets, осталось {} / {}",
|
||||||
*/
|
removedUsers, removedGuilds, userBuckets.size(), guildBuckets.size());
|
||||||
public void shutdown() {
|
}
|
||||||
logger.info("Завершение работы RateLimiter...");
|
}
|
||||||
cleanupExecutor.shutdown();
|
|
||||||
try {
|
/**
|
||||||
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
* Завершает работу rate limiter
|
||||||
cleanupExecutor.shutdownNow();
|
*/
|
||||||
}
|
public void shutdown() {
|
||||||
} catch (InterruptedException e) {
|
logger.info("Завершение работы RateLimiter...");
|
||||||
cleanupExecutor.shutdownNow();
|
cleanupExecutor.shutdown();
|
||||||
Thread.currentThread().interrupt();
|
try {
|
||||||
}
|
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
logger.info("RateLimiter завершён");
|
cleanupExecutor.shutdownNow();
|
||||||
}
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
/**
|
cleanupExecutor.shutdownNow();
|
||||||
* Token Bucket для rate limiting
|
Thread.currentThread().interrupt();
|
||||||
*/
|
}
|
||||||
private static class TokenBucket {
|
logger.info("RateLimiter завершён");
|
||||||
private final int capacity;
|
}
|
||||||
private final double refillRate; // токенов в секунду
|
|
||||||
private double tokens;
|
/**
|
||||||
private long lastRefillTime;
|
* Token Bucket для rate limiting
|
||||||
private long lastAccessTime;
|
*/
|
||||||
|
private static class TokenBucket {
|
||||||
public TokenBucket(int capacity, double refillRate) {
|
private final int capacity;
|
||||||
this.capacity = capacity;
|
private final double refillRate; // токенов в секунду
|
||||||
this.refillRate = refillRate;
|
private double tokens;
|
||||||
this.tokens = capacity;
|
private long lastRefillTime;
|
||||||
this.lastRefillTime = System.currentTimeMillis();
|
private long lastAccessTime;
|
||||||
this.lastAccessTime = System.currentTimeMillis();
|
|
||||||
}
|
public TokenBucket(int capacity, double refillRate) {
|
||||||
|
this.capacity = capacity;
|
||||||
public synchronized boolean tryConsume() {
|
this.refillRate = refillRate;
|
||||||
refill();
|
this.tokens = capacity;
|
||||||
lastAccessTime = System.currentTimeMillis();
|
this.lastRefillTime = System.currentTimeMillis();
|
||||||
|
this.lastAccessTime = System.currentTimeMillis();
|
||||||
if (tokens >= 1.0) {
|
}
|
||||||
tokens -= 1.0;
|
|
||||||
return true;
|
public synchronized boolean tryConsume() {
|
||||||
}
|
refill();
|
||||||
return false;
|
lastAccessTime = System.currentTimeMillis();
|
||||||
}
|
|
||||||
|
if (tokens >= 1.0) {
|
||||||
private void refill() {
|
tokens -= 1.0;
|
||||||
long now = System.currentTimeMillis();
|
return true;
|
||||||
long timePassed = now - lastRefillTime;
|
}
|
||||||
double tokensToAdd = (timePassed / 1000.0) * refillRate;
|
return false;
|
||||||
|
}
|
||||||
tokens = Math.min(capacity, tokens + tokensToAdd);
|
|
||||||
lastRefillTime = now;
|
private void refill() {
|
||||||
}
|
long now = System.currentTimeMillis();
|
||||||
|
long timePassed = now - lastRefillTime;
|
||||||
public synchronized long getWaitTime() {
|
double tokensToAdd = (timePassed / 1000.0) * refillRate;
|
||||||
refill();
|
|
||||||
if (tokens >= 1.0) {
|
tokens = Math.min(capacity, tokens + tokensToAdd);
|
||||||
return 0;
|
lastRefillTime = now;
|
||||||
}
|
}
|
||||||
double tokensNeeded = 1.0 - tokens;
|
|
||||||
return (long) Math.ceil(tokensNeeded / refillRate);
|
public synchronized long getWaitTime() {
|
||||||
}
|
refill();
|
||||||
|
if (tokens >= 1.0) {
|
||||||
public long getLastAccessTime() {
|
return 0;
|
||||||
return lastAccessTime;
|
}
|
||||||
}
|
double tokensNeeded = 1.0 - tokens;
|
||||||
}
|
return (long) Math.ceil(tokensNeeded / refillRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getLastAccessTime() {
|
||||||
|
return lastAccessTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,55 @@
|
||||||
package creator.loki.database;
|
package creator.loki.database;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
|
||||||
/**
|
|
||||||
* Менеджер SQLite базы данных
|
|
||||||
*/
|
|
||||||
public class Database {
|
public class Database {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(Database.class);
|
private static final Logger logger = LoggerFactory.getLogger(Database.class);
|
||||||
private final String dbPath;
|
private final String dbPath;
|
||||||
private Connection connection;
|
private HikariDataSource dataSource;
|
||||||
|
|
||||||
public Database(String dbPath) {
|
public Database(String dbPath) {
|
||||||
this.dbPath = dbPath;
|
this.dbPath = dbPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Подключиться к базе данных
|
|
||||||
*/
|
|
||||||
public void connect() {
|
public void connect() {
|
||||||
try {
|
HikariConfig config = new HikariConfig();
|
||||||
String url = "jdbc:sqlite:" + dbPath;
|
config.setJdbcUrl("jdbc:sqlite:" + dbPath);
|
||||||
connection = DriverManager.getConnection(url);
|
config.setMaximumPoolSize(5);
|
||||||
logger.info("Подключение к базе данных: {}", dbPath);
|
config.setConnectionTimeout(10000);
|
||||||
|
config.setIdleTimeout(300000);
|
||||||
// Включаем foreign keys
|
config.setMaxLifetime(600000);
|
||||||
try (Statement stmt = connection.createStatement()) {
|
config.setPoolName("LokiDB");
|
||||||
stmt.execute("PRAGMA foreign_keys = ON");
|
|
||||||
}
|
dataSource = new HikariDataSource(config);
|
||||||
|
logger.info("Подключение к базе данных: {} (pool size: {})", dbPath, config.getMaximumPoolSize());
|
||||||
|
|
||||||
|
try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) {
|
||||||
|
stmt.execute("PRAGMA foreign_keys = ON");
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
logger.error("Ошибка подключения к базе данных", e);
|
logger.error("Ошибка включения foreign keys", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Connection getConnection() {
|
||||||
|
try {
|
||||||
|
return dataSource.getConnection();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка получения соединения из пула", e);
|
||||||
throw new RuntimeException(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) {
|
public void execute(String sql) {
|
||||||
try (Statement stmt = getConnection().createStatement()) {
|
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||||||
stmt.execute(sql);
|
stmt.execute(sql);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
logger.error("Ошибка выполнения SQL: {}", sql, e);
|
logger.error("Ошибка выполнения SQL: {}", sql, e);
|
||||||
|
|
@ -64,29 +57,34 @@ public class Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Выполнить SQL запрос с параметрами
|
|
||||||
*/
|
|
||||||
public PreparedStatement prepare(String sql) {
|
public PreparedStatement prepare(String sql) {
|
||||||
try {
|
try {
|
||||||
return getConnection().prepareStatement(sql);
|
Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql);
|
||||||
|
return (PreparedStatement) Proxy.newProxyInstance(
|
||||||
|
PreparedStatement.class.getClassLoader(),
|
||||||
|
new Class<?>[]{PreparedStatement.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if (method.getName().equals("close")) {
|
||||||
|
try {
|
||||||
|
return method.invoke(stmt, args);
|
||||||
|
} finally {
|
||||||
|
conn.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return method.invoke(stmt, args);
|
||||||
|
}
|
||||||
|
);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
logger.error("Ошибка подготовки SQL: {}", sql, e);
|
logger.error("Ошибка подготовки SQL: {}", sql, e);
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Закрыть соединение
|
|
||||||
*/
|
|
||||||
public void close() {
|
public void close() {
|
||||||
try {
|
if (dataSource != null && !dataSource.isClosed()) {
|
||||||
if (connection != null && !connection.isClosed()) {
|
dataSource.close();
|
||||||
connection.close();
|
logger.info("Пул соединений закрыт");
|
||||||
logger.info("Соединение с базой данных закрыто");
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
logger.error("Ошибка закрытия соединения", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,191 +1,190 @@
|
||||||
package creator.loki.database;
|
package creator.loki.database;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Миграция данных из JSON в SQLite
|
* Миграция данных из JSON в SQLite
|
||||||
*/
|
*/
|
||||||
public class DatabaseMigration {
|
public class DatabaseMigration {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(DatabaseMigration.class);
|
private static final Logger logger = LoggerFactory.getLogger(DatabaseMigration.class);
|
||||||
private final Database database;
|
private final Database database;
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
public DatabaseMigration(Database database) {
|
public DatabaseMigration(Database database) {
|
||||||
this.database = database;
|
this.database = database;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Мигрировать данные из JSON
|
* Мигрировать данные из JSON
|
||||||
*/
|
*/
|
||||||
public void migrateFromJson(String jsonPath) {
|
public void migrateFromJson(String jsonPath) {
|
||||||
logger.info("Начало миграции данных из JSON: {}", jsonPath);
|
logger.info("Начало миграции данных из JSON: {}", jsonPath);
|
||||||
|
|
||||||
try (FileReader reader = new FileReader(jsonPath)) {
|
try (FileReader reader = new FileReader(jsonPath)) {
|
||||||
Type type = new TypeToken<Map<String, Object>>(){}.getType();
|
Type type = new TypeToken<Map<String, Object>>(){}.getType();
|
||||||
Map<String, Object> data = gson.fromJson(reader, type);
|
Map<String, Object> data = gson.fromJson(reader, type);
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
logger.warn("JSON файл пустой или не существует");
|
logger.warn("JSON файл пустой или не существует");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMigrated()) {
|
if (isMigrated()) {
|
||||||
logger.info("Данные уже мигрированы, пропускаем");
|
logger.info("Данные уже мигрированы, пропускаем");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
migrateUserStats(data);
|
migrateUserStats(data);
|
||||||
migrateReputation(data);
|
migrateReputation(data);
|
||||||
migrateShipPairs(data);
|
migrateShipPairs(data);
|
||||||
migrateCustomRoles(data);
|
migrateCustomRoles(data);
|
||||||
markMigrated();
|
markMigrated();
|
||||||
|
|
||||||
logger.info("Миграция завершена успешно");
|
logger.info("Миграция завершена успешно");
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Ошибка миграции данных", e);
|
logger.error("Ошибка миграции данных", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isMigrated() {
|
private boolean isMigrated() {
|
||||||
try (PreparedStatement stmt = database.prepare("SELECT value FROM config WHERE key = 'migrated'")) {
|
try (PreparedStatement stmt = database.prepare("SELECT value FROM config WHERE key = 'migrated'")) {
|
||||||
ResultSet rs = stmt.executeQuery();
|
ResultSet rs = stmt.executeQuery();
|
||||||
return rs.next() && "true".equals(rs.getString("value"));
|
return rs.next() && "true".equals(rs.getString("value"));
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markMigrated() {
|
private void markMigrated() {
|
||||||
try (PreparedStatement stmt = database.prepare(
|
try (PreparedStatement stmt = database.prepare(
|
||||||
"INSERT OR REPLACE INTO config (key, value) VALUES ('migrated', 'true')"
|
"INSERT OR REPLACE INTO config (key, value) VALUES ('migrated', 'true')"
|
||||||
)) {
|
)) {
|
||||||
stmt.executeUpdate();
|
stmt.executeUpdate();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
logger.error("Ошибка сохранения статуса миграции", e);
|
logger.error("Ошибка сохранения статуса миграции", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private void migrateUserStats(Map<String, Object> data) {
|
private void migrateUserStats(Map<String, Object> data) {
|
||||||
Map<String, Object> userStats = (Map<String, Object>) data.get("userStats");
|
Map<String, Object> userStats = (Map<String, Object>) data.get("userStats");
|
||||||
if (userStats == null) return;
|
if (userStats == null) return;
|
||||||
|
|
||||||
logger.info("Миграция userStats: {} записей", userStats.size());
|
logger.info("Миграция userStats: {} записей", userStats.size());
|
||||||
|
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT OR REPLACE INTO user_stats
|
INSERT OR REPLACE INTO user_stats
|
||||||
(user_id, level, total_xp, message_count, voice_minutes, last_message_time, last_voice_join)
|
(user_id, level, total_xp, message_count, voice_minutes, last_message_time, last_voice_join)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)""";
|
||||||
""";
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
try (PreparedStatement stmt = database.prepare(sql)) {
|
for (Map.Entry<String, Object> entry : userStats.entrySet()) {
|
||||||
for (Map.Entry<String, Object> entry : userStats.entrySet()) {
|
Map<String, Object> stats = (Map<String, Object>) entry.getValue();
|
||||||
Map<String, Object> stats = (Map<String, Object>) entry.getValue();
|
|
||||||
|
stmt.setString(1, (String) stats.get("userId"));
|
||||||
stmt.setString(1, (String) stats.get("userId"));
|
stmt.setInt(2, ((Number) stats.get("level")).intValue());
|
||||||
stmt.setInt(2, ((Number) stats.get("level")).intValue());
|
stmt.setInt(3, ((Number) stats.get("totalXP")).intValue());
|
||||||
stmt.setInt(3, ((Number) stats.get("totalXP")).intValue());
|
stmt.setInt(4, ((Number) stats.get("messageCount")).intValue());
|
||||||
stmt.setInt(4, ((Number) stats.get("messageCount")).intValue());
|
stmt.setLong(5, ((Number) stats.get("voiceMinutes")).longValue());
|
||||||
stmt.setLong(5, ((Number) stats.get("voiceMinutes")).longValue());
|
stmt.setLong(6, ((Number) stats.get("lastMessageTime")).longValue());
|
||||||
stmt.setLong(6, ((Number) stats.get("lastMessageTime")).longValue());
|
stmt.setLong(7, ((Number) stats.get("lastVoiceJoin")).longValue());
|
||||||
stmt.setLong(7, ((Number) stats.get("lastVoiceJoin")).longValue());
|
|
||||||
|
stmt.executeUpdate();
|
||||||
stmt.executeUpdate();
|
}
|
||||||
}
|
logger.info("userStats мигрированы");
|
||||||
logger.info("userStats мигрированы");
|
} catch (SQLException e) {
|
||||||
} catch (SQLException e) {
|
logger.error("Ошибка миграции userStats", e);
|
||||||
logger.error("Ошибка миграции userStats", e);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
@SuppressWarnings("unchecked")
|
private void migrateReputation(Map<String, Object> data) {
|
||||||
private void migrateReputation(Map<String, Object> data) {
|
Map<String, Object> reputation = (Map<String, Object>) data.get("reputation");
|
||||||
Map<String, Object> reputation = (Map<String, Object>) data.get("reputation");
|
if (reputation == null) return;
|
||||||
if (reputation == null) return;
|
|
||||||
|
logger.info("Миграция reputation: {} записей", reputation.size());
|
||||||
logger.info("Миграция reputation: {} записей", reputation.size());
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(
|
||||||
try (PreparedStatement stmt = database.prepare(
|
"INSERT OR REPLACE INTO reputation (user_id, reputation) VALUES (?, ?)"
|
||||||
"INSERT OR REPLACE INTO reputation (user_id, reputation) VALUES (?, ?)"
|
)) {
|
||||||
)) {
|
for (Map.Entry<String, Object> entry : reputation.entrySet()) {
|
||||||
for (Map.Entry<String, Object> entry : reputation.entrySet()) {
|
stmt.setString(1, entry.getKey());
|
||||||
stmt.setString(1, entry.getKey());
|
stmt.setDouble(2, ((Number) entry.getValue()).doubleValue());
|
||||||
stmt.setDouble(2, ((Number) entry.getValue()).doubleValue());
|
stmt.executeUpdate();
|
||||||
stmt.executeUpdate();
|
}
|
||||||
}
|
logger.info("reputation мигрирована");
|
||||||
logger.info("reputation мигрирована");
|
} catch (SQLException e) {
|
||||||
} catch (SQLException e) {
|
logger.error("Ошибка миграции reputation", e);
|
||||||
logger.error("Ошибка миграции reputation", e);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
@SuppressWarnings("unchecked")
|
private void migrateShipPairs(Map<String, Object> data) {
|
||||||
private void migrateShipPairs(Map<String, Object> data) {
|
Map<String, Object> shipPairs = (Map<String, Object>) data.get("shipPairs");
|
||||||
Map<String, Object> shipPairs = (Map<String, Object>) data.get("shipPairs");
|
if (shipPairs == null) return;
|
||||||
if (shipPairs == null) return;
|
|
||||||
|
logger.info("Миграция shipPairs: {} записей", shipPairs.size());
|
||||||
logger.info("Миграция shipPairs: {} записей", shipPairs.size());
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(
|
||||||
try (PreparedStatement stmt = database.prepare(
|
"INSERT OR REPLACE INTO ship_pairs (user1_id, user2_id, compatibility) VALUES (?, ?, ?)"
|
||||||
"INSERT OR REPLACE INTO ship_pairs (user1_id, user2_id, compatibility) VALUES (?, ?, ?)"
|
)) {
|
||||||
)) {
|
for (Map.Entry<String, Object> entry : shipPairs.entrySet()) {
|
||||||
for (Map.Entry<String, Object> entry : shipPairs.entrySet()) {
|
String[] users = entry.getKey().split("-");
|
||||||
String[] users = entry.getKey().split("-");
|
if (users.length == 2) {
|
||||||
if (users.length == 2) {
|
stmt.setString(1, users[0]);
|
||||||
stmt.setString(1, users[0]);
|
stmt.setString(2, users[1]);
|
||||||
stmt.setString(2, users[1]);
|
stmt.setDouble(3, ((Number) entry.getValue()).doubleValue());
|
||||||
stmt.setDouble(3, ((Number) entry.getValue()).doubleValue());
|
stmt.executeUpdate();
|
||||||
stmt.executeUpdate();
|
}
|
||||||
}
|
}
|
||||||
}
|
logger.info("shipPairs мигрированы");
|
||||||
logger.info("shipPairs мигрированы");
|
} catch (SQLException e) {
|
||||||
} catch (SQLException e) {
|
logger.error("Ошибка миграции shipPairs", e);
|
||||||
logger.error("Ошибка миграции shipPairs", e);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
@SuppressWarnings("unchecked")
|
private void migrateCustomRoles(Map<String, Object> data) {
|
||||||
private void migrateCustomRoles(Map<String, Object> data) {
|
Map<String, Object> customRoles = (Map<String, Object>) data.get("custom_roles");
|
||||||
Map<String, Object> customRoles = (Map<String, Object>) data.get("custom_roles");
|
if (customRoles == null) return;
|
||||||
if (customRoles == null) return;
|
|
||||||
|
logger.info("Миграция custom_roles");
|
||||||
logger.info("Миграция custom_roles");
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(
|
||||||
try (PreparedStatement stmt = database.prepare(
|
"INSERT OR REPLACE INTO custom_roles (guild_id, user_id, role_id) VALUES (?, ?, ?)"
|
||||||
"INSERT OR REPLACE INTO custom_roles (guild_id, user_id, role_id) VALUES (?, ?, ?)"
|
)) {
|
||||||
)) {
|
for (Map.Entry<String, Object> guildEntry : customRoles.entrySet()) {
|
||||||
for (Map.Entry<String, Object> guildEntry : customRoles.entrySet()) {
|
String guildId = guildEntry.getKey();
|
||||||
String guildId = guildEntry.getKey();
|
Map<String, Object> users = (Map<String, Object>) guildEntry.getValue();
|
||||||
Map<String, Object> users = (Map<String, Object>) guildEntry.getValue();
|
|
||||||
|
for (Map.Entry<String, Object> userEntry : users.entrySet()) {
|
||||||
for (Map.Entry<String, Object> userEntry : users.entrySet()) {
|
String userId = userEntry.getKey();
|
||||||
String userId = userEntry.getKey();
|
Object rolesObj = userEntry.getValue();
|
||||||
Object rolesObj = userEntry.getValue();
|
|
||||||
|
if (rolesObj instanceof java.util.List<?> rolesList) {
|
||||||
if (rolesObj instanceof java.util.List) {
|
for (Object roleId : rolesList) {
|
||||||
for (Object roleId : (java.util.List<?>) rolesObj) {
|
stmt.setString(1, guildId);
|
||||||
stmt.setString(1, guildId);
|
stmt.setString(2, userId);
|
||||||
stmt.setString(2, userId);
|
stmt.setString(3, roleId.toString());
|
||||||
stmt.setString(3, roleId.toString());
|
stmt.executeUpdate();
|
||||||
stmt.executeUpdate();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
logger.info("custom_roles мигрированы");
|
||||||
logger.info("custom_roles мигрированы");
|
} catch (SQLException e) {
|
||||||
} catch (SQLException e) {
|
logger.error("Ошибка миграции custom_roles", e);
|
||||||
logger.error("Ошибка миграции custom_roles", e);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,112 +1,249 @@
|
||||||
package creator.loki.database;
|
package creator.loki.database;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Схема базы данных - создание таблиц
|
* Схема базы данных - создание таблиц
|
||||||
*/
|
*/
|
||||||
public class DatabaseSchema {
|
public class DatabaseSchema {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(DatabaseSchema.class);
|
private static final Logger logger = LoggerFactory.getLogger(DatabaseSchema.class);
|
||||||
private final Database database;
|
private final Database database;
|
||||||
|
|
||||||
public DatabaseSchema(Database database) {
|
public DatabaseSchema(Database database) {
|
||||||
this.database = database;
|
this.database = database;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Создать все таблицы
|
* Создать все таблицы
|
||||||
*/
|
*/
|
||||||
public void createTables() {
|
public void createTables() {
|
||||||
logger.info("Создание таблиц базы данных...");
|
logger.info("Создание таблиц базы данных...");
|
||||||
|
|
||||||
createConfigTable();
|
createConfigTable();
|
||||||
createUserStatsTable();
|
createUserStatsTable();
|
||||||
createReputationTable();
|
createReputationTable();
|
||||||
createShipPairsTable();
|
createShipPairsTable();
|
||||||
createCustomRolesTable();
|
createCustomRolesTable();
|
||||||
createVoiceChannelsTable();
|
createVoiceChannelsTable();
|
||||||
createCounterStatsTable();
|
createVoicePresetsTable();
|
||||||
|
createCounterStatsTable();
|
||||||
logger.info("Таблицы созданы успешно");
|
createRatingsTable();
|
||||||
}
|
createWarningsTable();
|
||||||
|
createModerationCasesTable();
|
||||||
private void createConfigTable() {
|
createActiveMutesTable();
|
||||||
database.execute("""
|
createEconomyTable();
|
||||||
CREATE TABLE IF NOT EXISTS config (
|
|
||||||
key TEXT PRIMARY KEY,
|
createIndexes();
|
||||||
value TEXT NOT NULL
|
|
||||||
)
|
logger.info("Таблицы созданы успешно");
|
||||||
""");
|
}
|
||||||
}
|
|
||||||
|
private void createConfigTable() {
|
||||||
private void createUserStatsTable() {
|
database.execute("""
|
||||||
database.execute("""
|
CREATE TABLE IF NOT EXISTS config (
|
||||||
CREATE TABLE IF NOT EXISTS user_stats (
|
key TEXT PRIMARY KEY,
|
||||||
user_id TEXT PRIMARY KEY,
|
value TEXT NOT NULL
|
||||||
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,
|
private void createUserStatsTable() {
|
||||||
last_voice_join INTEGER DEFAULT 0
|
database.execute("""
|
||||||
)
|
CREATE TABLE IF NOT EXISTS user_stats (
|
||||||
""");
|
user_id TEXT PRIMARY KEY,
|
||||||
}
|
level INTEGER DEFAULT 0,
|
||||||
|
total_xp INTEGER DEFAULT 0,
|
||||||
private void createReputationTable() {
|
message_count INTEGER DEFAULT 0,
|
||||||
database.execute("""
|
voice_minutes INTEGER DEFAULT 0,
|
||||||
CREATE TABLE IF NOT EXISTS reputation (
|
last_message_time INTEGER DEFAULT 0,
|
||||||
user_id TEXT PRIMARY KEY,
|
last_voice_join INTEGER DEFAULT 0
|
||||||
reputation REAL DEFAULT 0.0
|
)
|
||||||
)
|
""");
|
||||||
""");
|
}
|
||||||
}
|
|
||||||
|
private void createReputationTable() {
|
||||||
private void createShipPairsTable() {
|
database.execute("""
|
||||||
database.execute("""
|
CREATE TABLE IF NOT EXISTS reputation (
|
||||||
CREATE TABLE IF NOT EXISTS ship_pairs (
|
user_id TEXT PRIMARY KEY,
|
||||||
user1_id TEXT NOT NULL,
|
reputation REAL DEFAULT 0.0
|
||||||
user2_id TEXT NOT NULL,
|
)
|
||||||
compatibility REAL NOT NULL,
|
""");
|
||||||
PRIMARY KEY (user1_id, user2_id)
|
}
|
||||||
)
|
|
||||||
""");
|
private void createShipPairsTable() {
|
||||||
}
|
database.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS ship_pairs (
|
||||||
private void createCustomRolesTable() {
|
user1_id TEXT NOT NULL,
|
||||||
database.execute("""
|
user2_id TEXT NOT NULL,
|
||||||
CREATE TABLE IF NOT EXISTS custom_roles (
|
compatibility REAL NOT NULL,
|
||||||
guild_id TEXT NOT NULL,
|
PRIMARY KEY (user1_id, user2_id)
|
||||||
user_id TEXT NOT NULL,
|
)
|
||||||
role_id TEXT NOT NULL,
|
""");
|
||||||
PRIMARY KEY (guild_id, user_id, role_id)
|
}
|
||||||
)
|
|
||||||
""");
|
private void createCustomRolesTable() {
|
||||||
}
|
database.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS custom_roles (
|
||||||
private void createVoiceChannelsTable() {
|
guild_id TEXT NOT NULL,
|
||||||
database.execute("""
|
user_id TEXT NOT NULL,
|
||||||
CREATE TABLE IF NOT EXISTS voice_channels (
|
role_id TEXT NOT NULL,
|
||||||
channel_id TEXT PRIMARY KEY,
|
PRIMARY KEY (guild_id, user_id, role_id)
|
||||||
owner_id TEXT NOT NULL,
|
)
|
||||||
created_at INTEGER NOT NULL,
|
""");
|
||||||
name TEXT,
|
}
|
||||||
user_limit INTEGER DEFAULT 0,
|
|
||||||
bitrate INTEGER DEFAULT 64000,
|
private void createVoiceChannelsTable() {
|
||||||
locked INTEGER DEFAULT 0
|
database.execute("""
|
||||||
)
|
CREATE TABLE IF NOT EXISTS voice_channels (
|
||||||
""");
|
channel_id TEXT PRIMARY KEY,
|
||||||
}
|
owner_id TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
private void createCounterStatsTable() {
|
name TEXT,
|
||||||
database.execute("""
|
user_limit INTEGER DEFAULT 0,
|
||||||
CREATE TABLE IF NOT EXISTS counter_stats (
|
bitrate INTEGER DEFAULT 64000,
|
||||||
date TEXT PRIMARY KEY,
|
locked INTEGER DEFAULT 0,
|
||||||
joined INTEGER DEFAULT 0,
|
text_channel_id TEXT
|
||||||
left_count INTEGER DEFAULT 0,
|
)
|
||||||
difference INTEGER DEFAULT 0
|
""");
|
||||||
)
|
}
|
||||||
""");
|
|
||||||
}
|
private void createVoicePresetsTable() {
|
||||||
}
|
database.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS voice_presets (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
user_limit INTEGER DEFAULT 0,
|
||||||
|
bitrate INTEGER DEFAULT 64000,
|
||||||
|
region TEXT DEFAULT 'auto',
|
||||||
|
locked INTEGER DEFAULT 0,
|
||||||
|
allowed_role_id TEXT,
|
||||||
|
denied_role_id TEXT
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createRatingsTable() {
|
||||||
|
database.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS ratings (
|
||||||
|
guild_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
rating_type TEXT NOT NULL,
|
||||||
|
value INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (guild_id, user_id, rating_type)
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createWarningsTable() {
|
||||||
|
database.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS warnings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
guild_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
moderator_id TEXT NOT NULL,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createModerationCasesTable() {
|
||||||
|
database.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS moderation_cases (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
guild_id TEXT NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
target_id TEXT NOT NULL,
|
||||||
|
moderator_id TEXT NOT NULL,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
duration_sec INTEGER,
|
||||||
|
until_ms INTEGER
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createActiveMutesTable() {
|
||||||
|
database.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS active_mutes (
|
||||||
|
guild_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
ends_at_ms INTEGER NOT NULL,
|
||||||
|
case_id INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (guild_id, user_id)
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createEconomyTable() {
|
||||||
|
database.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS economy (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
cash INTEGER NOT NULL DEFAULT 0,
|
||||||
|
bank INTEGER NOT NULL DEFAULT 0,
|
||||||
|
total_earned INTEGER NOT NULL DEFAULT 0,
|
||||||
|
total_spent INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_daily TEXT,
|
||||||
|
daily_streak INTEGER DEFAULT 0,
|
||||||
|
fishing_luck INTEGER DEFAULT 0,
|
||||||
|
efficiency INTEGER DEFAULT 0,
|
||||||
|
last_fish TEXT,
|
||||||
|
job TEXT,
|
||||||
|
last_work TEXT
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создать индексы для ускорения запросов
|
||||||
|
*/
|
||||||
|
private void createIndexes() {
|
||||||
|
logger.info("Создание индексов...");
|
||||||
|
|
||||||
|
// Индексы для user_stats
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_level ON user_stats(level DESC)");
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_total_xp ON user_stats(total_xp DESC)");
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_message_count ON user_stats(message_count DESC)");
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_voice_minutes ON user_stats(voice_minutes DESC)");
|
||||||
|
|
||||||
|
// Индексы для reputation
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_reputation_value ON reputation(reputation DESC)");
|
||||||
|
|
||||||
|
// Индексы для ratings
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_ratings_guild_type ON ratings(guild_id, rating_type)");
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_ratings_value ON ratings(value DESC)");
|
||||||
|
|
||||||
|
// Индексы для voice_channels
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_voice_channels_owner ON voice_channels(owner_id)");
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_voice_channels_created ON voice_channels(created_at)");
|
||||||
|
|
||||||
|
// Индексы для custom_roles
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_custom_roles_guild ON custom_roles(guild_id)");
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_custom_roles_user ON custom_roles(user_id)");
|
||||||
|
|
||||||
|
// Индексы для counter_stats
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_counter_stats_date ON counter_stats(date DESC)");
|
||||||
|
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_warnings_guild_user ON warnings(guild_id, user_id)");
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_warnings_guild_time ON warnings(guild_id, created_at DESC)");
|
||||||
|
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_mod_cases_guild_id ON moderation_cases(guild_id, id DESC)");
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_active_mutes_ends ON active_mutes(ends_at_ms)");
|
||||||
|
|
||||||
|
database.execute("CREATE INDEX IF NOT EXISTS idx_economy_balance ON economy((cash + bank) DESC)");
|
||||||
|
|
||||||
|
logger.info("Индексы созданы успешно");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,46 +3,78 @@ package creator.loki.di;
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import com.google.inject.Provides;
|
import com.google.inject.Provides;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
import creator.loki.config.ConfigLoader;
|
import creator.loki.config.ConfigLoader;
|
||||||
import creator.loki.core.EventDispatcher;
|
import creator.loki.core.EventDispatcher;
|
||||||
|
import creator.loki.core.ListenerAggregator;
|
||||||
|
import creator.loki.core.MessageCache;
|
||||||
|
import creator.loki.core.ShutdownManager;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
import creator.loki.core.registry.CommandRegistry;
|
import creator.loki.core.registry.CommandRegistry;
|
||||||
import creator.loki.core.registry.SlashCommandRegistry;
|
import creator.loki.core.registry.PrefixCommandRegistrar;
|
||||||
import creator.loki.core.security.CooldownManager;
|
import creator.loki.core.security.CooldownManager;
|
||||||
import creator.loki.core.security.RateLimiter;
|
import creator.loki.core.security.RateLimiter;
|
||||||
import creator.loki.feature.counter.CounterManager;
|
|
||||||
import creator.loki.data.JsonDataStore;
|
|
||||||
import creator.loki.database.ConfigRepository;
|
|
||||||
import creator.loki.database.Database;
|
import creator.loki.database.Database;
|
||||||
import creator.loki.database.DatabaseMigration;
|
import creator.loki.database.DatabaseMigration;
|
||||||
import creator.loki.database.DatabaseSchema;
|
import creator.loki.database.DatabaseSchema;
|
||||||
import creator.loki.feature.level.domain.XpFormula;
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
import creator.loki.feature.level.LevelUpNotifier;
|
import creator.loki.feature.config.repository.PrefixedGuildConfigRepository;
|
||||||
import creator.loki.feature.level.service.LeaderboardService;
|
import creator.loki.feature.rating.repository.RatingsRepository;
|
||||||
import creator.loki.feature.level.service.LevelService;
|
import creator.loki.feature.reputation.repository.ReputationRepository;
|
||||||
import creator.loki.feature.level.service.MessageCooldownService;
|
import creator.loki.feature.ship.repository.ShipPairRepository;
|
||||||
import creator.loki.feature.level.VoiceTracker;
|
import creator.loki.feature.counter.CounterModule;
|
||||||
import creator.loki.level.LevelSystem;
|
import creator.loki.feature.entertainment.EntertainmentCommandRegistrar;
|
||||||
import creator.loki.metrics.MetricsCollector;
|
import creator.loki.feature.level.LevelCommandRegistrar;
|
||||||
import creator.loki.profile.ProfileService;
|
import creator.loki.feature.level.LevelModule;
|
||||||
import creator.loki.repository.UserStatsRepository;
|
import creator.loki.feature.metrics.MetricsCollector;
|
||||||
import creator.loki.repository.impl.JsonUserStatsRepository;
|
import creator.loki.feature.metrics.HealthHttpServer;
|
||||||
import creator.loki.feature.role.CustomRoleManager;
|
import creator.loki.feature.metrics.MetricsModule;
|
||||||
import creator.loki.feature.voice.VoiceChannelManager;
|
import creator.loki.feature.moderation.ModerationCommandRegistrar;
|
||||||
|
import creator.loki.feature.moderation.ModerationModule;
|
||||||
|
import creator.loki.feature.profile.ProfileModule;
|
||||||
|
import creator.loki.feature.role.RoleModule;
|
||||||
|
import creator.loki.feature.utility.UtilityCommandRegistrar;
|
||||||
|
import creator.loki.feature.music.MusicModule;
|
||||||
|
import creator.loki.feature.music.MusicCommandRegistrar;
|
||||||
|
import creator.loki.feature.voice.VoiceModule;
|
||||||
|
import creator.loki.feature.welcome.WelcomeModule;
|
||||||
|
import creator.loki.feature.config.repository.GuildConfigRepository;
|
||||||
|
import creator.loki.feature.economy.EconomyCommandRegistrar;
|
||||||
|
import creator.loki.feature.economy.EconomyModule;
|
||||||
|
|
||||||
/**
|
|
||||||
* Главный модуль Guice для конфигурации Dependency Injection
|
|
||||||
*/
|
|
||||||
public class BotModule extends AbstractModule {
|
public class BotModule extends AbstractModule {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure() {
|
protected void configure() {
|
||||||
// Все основные компоненты - Singleton
|
|
||||||
bind(CommandRegistry.class).in(Singleton.class);
|
bind(CommandRegistry.class).in(Singleton.class);
|
||||||
bind(SlashCommandRegistry.class).in(Singleton.class);
|
|
||||||
bind(CooldownManager.class).in(Singleton.class);
|
bind(CooldownManager.class).in(Singleton.class);
|
||||||
bind(RateLimiter.class).in(Singleton.class);
|
bind(RateLimiter.class).in(Singleton.class);
|
||||||
bind(MetricsCollector.class).in(Singleton.class);
|
bind(MetricsCollector.class).in(Singleton.class);
|
||||||
|
bind(HealthHttpServer.class).in(Singleton.class);
|
||||||
|
bind(ListenerAggregator.class).in(Singleton.class);
|
||||||
|
bind(MessageCache.class).in(Singleton.class);
|
||||||
|
bind(ShutdownManager.class).in(Singleton.class);
|
||||||
|
bind(PrefixCommandRegistrar.class).in(Singleton.class);
|
||||||
|
|
||||||
|
install(new VoiceModule());
|
||||||
|
install(new MusicModule());
|
||||||
|
install(new LevelModule());
|
||||||
|
install(new ModerationModule());
|
||||||
|
install(new RoleModule());
|
||||||
|
install(new CounterModule());
|
||||||
|
install(new ProfileModule());
|
||||||
|
install(new MetricsModule());
|
||||||
|
install(new WelcomeModule());
|
||||||
|
install(new EconomyModule());
|
||||||
|
|
||||||
|
Multibinder<CommandRegistrar> commandBinder = Multibinder.newSetBinder(binder(), CommandRegistrar.class);
|
||||||
|
commandBinder.addBinding().to(LevelCommandRegistrar.class);
|
||||||
|
commandBinder.addBinding().to(ModerationCommandRegistrar.class);
|
||||||
|
commandBinder.addBinding().to(EntertainmentCommandRegistrar.class);
|
||||||
|
commandBinder.addBinding().to(UtilityCommandRegistrar.class);
|
||||||
|
commandBinder.addBinding().to(MusicCommandRegistrar.class);
|
||||||
|
commandBinder.addBinding().to(EconomyCommandRegistrar.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
@ -51,38 +83,6 @@ public class BotModule extends AbstractModule {
|
||||||
return ConfigLoader.load();
|
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(LevelService levelService) {
|
|
||||||
return new VoiceTracker(levelService);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
ProfileService provideProfileService(JsonDataStore dataStore) {
|
|
||||||
return new ProfileService(dataStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
EventDispatcher provideEventDispatcher(
|
EventDispatcher provideEventDispatcher(
|
||||||
|
|
@ -96,18 +96,13 @@ public class BotModule extends AbstractModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
Database provideDatabase() {
|
Database provideDatabase(BotConfig config) {
|
||||||
Database database = new Database("data/bot.db");
|
Database database = new Database(config.getDbPath());
|
||||||
database.connect();
|
database.connect();
|
||||||
|
|
||||||
// Создаем таблицы
|
|
||||||
DatabaseSchema schema = new DatabaseSchema(database);
|
DatabaseSchema schema = new DatabaseSchema(database);
|
||||||
schema.createTables();
|
schema.createTables();
|
||||||
|
|
||||||
// Мигрируем данные из JSON если нужно
|
|
||||||
DatabaseMigration migration = new DatabaseMigration(database);
|
DatabaseMigration migration = new DatabaseMigration(database);
|
||||||
migration.migrateFromJson("data/bot_data.json");
|
migration.migrateFromJson("data/bot_data.json");
|
||||||
|
|
||||||
return database;
|
return database;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,69 +114,25 @@ public class BotModule extends AbstractModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
LevelUpNotifier provideLevelUpNotifier(ConfigRepository configRepo, LevelService levelService) {
|
GuildConfigRepository provideGuildConfigRepository(ConfigRepository configRepository) {
|
||||||
return new LevelUpNotifier(configRepo, levelService);
|
return new PrefixedGuildConfigRepository(configRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
VoiceChannelManager provideVoiceChannelManager(JsonDataStore dataStore, ConfigRepository configRepo) {
|
RatingsRepository provideRatingsRepository(Database database) {
|
||||||
return new VoiceChannelManager(dataStore, configRepo);
|
return new RatingsRepository(database);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
UserStatsRepository provideUserStatsRepository(JsonDataStore dataStore) {
|
ReputationRepository provideReputationRepository(Database database) {
|
||||||
return new JsonUserStatsRepository(dataStore);
|
return new ReputationRepository(database);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
CounterManager provideCounterManager(JsonDataStore dataStore, ConfigRepository configRepo) {
|
ShipPairRepository provideShipPairRepository(Database database) {
|
||||||
return new CounterManager(dataStore, configRepo);
|
return new ShipPairRepository(database);
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
XpFormula provideXpFormula() {
|
|
||||||
return new XpFormula();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
MessageCooldownService provideMessageCooldownService(UserStatsRepository repository) {
|
|
||||||
return new MessageCooldownService(repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
LeaderboardService provideLeaderboardService(UserStatsRepository repository) {
|
|
||||||
return new LeaderboardService(repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
LevelService provideLevelService(
|
|
||||||
UserStatsRepository repository,
|
|
||||||
XpFormula xpFormula,
|
|
||||||
MessageCooldownService cooldownService
|
|
||||||
) {
|
|
||||||
return new LevelService(repository, xpFormula, cooldownService);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
ProfileService provideOldProfileService(JsonDataStore dataStore) {
|
|
||||||
return new ProfileService(dataStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
creator.loki.feature.profile.ProfileService provideNewProfileService(
|
|
||||||
UserStatsRepository repository,
|
|
||||||
LevelService levelService,
|
|
||||||
LeaderboardService leaderboardService
|
|
||||||
) {
|
|
||||||
return new creator.loki.feature.profile.ProfileService(repository, levelService, leaderboardService);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,79 @@
|
||||||
package creator.loki.embed;
|
package creator.loki.embed;
|
||||||
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
public class EmbedFactory {
|
public class EmbedFactory {
|
||||||
private final EmbedBuilder builder;
|
private final EmbedBuilder builder;
|
||||||
|
|
||||||
private EmbedFactory() {
|
private EmbedFactory() {
|
||||||
this.builder = new EmbedBuilder();
|
this.builder = new EmbedBuilder();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static EmbedFactory create() {
|
public static EmbedFactory create() {
|
||||||
return new EmbedFactory();
|
return new EmbedFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedFactory setTitle(String title) {
|
public EmbedFactory withTitle(String title) {
|
||||||
builder.setTitle(title);
|
builder.setTitle(title);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedFactory setDescription(String description) {
|
public EmbedFactory withDescription(String description) {
|
||||||
builder.setDescription(description);
|
builder.setDescription(description);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedFactory setColor(Color color) {
|
public EmbedFactory withColor(Color color) {
|
||||||
builder.setColor(color);
|
builder.setColor(color);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedFactory addField(String name, String value, boolean inline) {
|
public EmbedFactory addField(String name, String value, boolean inline) {
|
||||||
builder.addField(name, value, inline);
|
builder.addField(name, value, inline);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedFactory setFooter(String text) {
|
public EmbedFactory withFooter(String text) {
|
||||||
builder.setFooter(text);
|
builder.setFooter(text);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedFactory setFooter(String text, String iconUrl) {
|
public EmbedFactory withFooter(String text, String iconUrl) {
|
||||||
builder.setFooter(text, iconUrl);
|
builder.setFooter(text, iconUrl);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedFactory setThumbnail(String url) {
|
public EmbedFactory withThumbnail(String url) {
|
||||||
builder.setThumbnail(url);
|
builder.setThumbnail(url);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedFactory setImage(String url) {
|
public EmbedFactory withImage(String url) {
|
||||||
builder.setImage(url);
|
builder.setImage(url);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedFactory setAuthor(String name) {
|
public EmbedFactory withAuthor(String name) {
|
||||||
builder.setAuthor(name);
|
builder.setAuthor(name);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedFactory setAuthor(String name, String url) {
|
public EmbedFactory withAuthor(String name, String url) {
|
||||||
builder.setAuthor(name, url);
|
builder.setAuthor(name, url);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedFactory setAuthor(String name, String url, String iconUrl) {
|
public EmbedFactory withAuthor(String name, String url, String iconUrl) {
|
||||||
builder.setAuthor(name, url, iconUrl);
|
builder.setAuthor(name, url, iconUrl);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageEmbed build() {
|
public MessageEmbed build() {
|
||||||
builder.setTimestamp(Instant.now());
|
builder.setTimestamp(Instant.now());
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,50 @@
|
||||||
package creator.loki.embed;
|
package creator.loki.embed;
|
||||||
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
public final class EmbedTemplates {
|
public final class EmbedTemplates {
|
||||||
|
|
||||||
private EmbedTemplates() {
|
private EmbedTemplates() {
|
||||||
// Запрет создания экземпляров
|
// Запрет создания экземпляров
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MessageEmbed success(String title, String description) {
|
public static MessageEmbed success(String title, String description) {
|
||||||
return new EmbedBuilder()
|
return new EmbedBuilder()
|
||||||
.setColor(Color.GREEN)
|
.setColor(Color.GREEN)
|
||||||
.setTitle("✅ " + title)
|
.setTitle("✅ " + title)
|
||||||
.setDescription(description)
|
.setDescription(description)
|
||||||
.setTimestamp(Instant.now())
|
.setTimestamp(Instant.now())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MessageEmbed error(String title, String description) {
|
public static MessageEmbed error(String title, String description) {
|
||||||
return new EmbedBuilder()
|
return new EmbedBuilder()
|
||||||
.setColor(Color.RED)
|
.setColor(Color.RED)
|
||||||
.setTitle("❌ " + title)
|
.setTitle("❌ " + title)
|
||||||
.setDescription(description)
|
.setDescription(description)
|
||||||
.setTimestamp(Instant.now())
|
.setTimestamp(Instant.now())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MessageEmbed info(String title, String description) {
|
public static MessageEmbed info(String title, String description) {
|
||||||
return new EmbedBuilder()
|
return new EmbedBuilder()
|
||||||
.setColor(Color.CYAN)
|
.setColor(Color.CYAN)
|
||||||
.setTitle("ℹ️ " + title)
|
.setTitle("ℹ️ " + title)
|
||||||
.setDescription(description)
|
.setDescription(description)
|
||||||
.setTimestamp(Instant.now())
|
.setTimestamp(Instant.now())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MessageEmbed warning(String title, String description) {
|
public static MessageEmbed warning(String title, String description) {
|
||||||
return new EmbedBuilder()
|
return new EmbedBuilder()
|
||||||
.setColor(Color.ORANGE)
|
.setColor(Color.ORANGE)
|
||||||
.setTitle("⚠️ " + title)
|
.setTitle("⚠️ " + title)
|
||||||
.setDescription(description)
|
.setDescription(description)
|
||||||
.setTimestamp(Instant.now())
|
.setTimestamp(Instant.now())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
75
src/main/java/creator/loki/embed/HelpCategories.java
Normal file
75
src/main/java/creator/loki/embed/HelpCategories.java
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
package creator.loki.embed;
|
||||||
|
|
||||||
|
import creator.loki.command.CommandCategories;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class HelpCategories {
|
||||||
|
private static final Map<String, String> EMOJIS;
|
||||||
|
private static final Map<String, Color> COLORS;
|
||||||
|
private static final Map<String, String> DESCRIPTIONS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
EMOJIS = Map.ofEntries(
|
||||||
|
Map.entry("Общие", "📋"),
|
||||||
|
Map.entry("Роли", "🎭"),
|
||||||
|
Map.entry("Развлечения", "🎮"),
|
||||||
|
Map.entry(CommandCategories.GIF_INTERACTIVE, "💞"),
|
||||||
|
Map.entry("Модерация", "🛡️"),
|
||||||
|
Map.entry("Статистика", "📊"),
|
||||||
|
Map.entry("Утилиты", "🔧"),
|
||||||
|
Map.entry("Система", "💠"),
|
||||||
|
Map.entry("Настройка", "⚙️"),
|
||||||
|
Map.entry("Супер-Админ", "🔐")
|
||||||
|
);
|
||||||
|
|
||||||
|
COLORS = Map.ofEntries(
|
||||||
|
Map.entry("Общие", new Color(52, 152, 219)),
|
||||||
|
Map.entry("Роли", new Color(155, 89, 182)),
|
||||||
|
Map.entry("Развлечения", new Color(241, 196, 15)),
|
||||||
|
Map.entry(CommandCategories.GIF_INTERACTIVE, new Color(233, 30, 99)),
|
||||||
|
Map.entry("Модерация", new Color(231, 76, 60)),
|
||||||
|
Map.entry("Статистика", new Color(46, 204, 113)),
|
||||||
|
Map.entry("Утилиты", new Color(52, 152, 219)),
|
||||||
|
Map.entry("Система", new Color(52, 73, 94)),
|
||||||
|
Map.entry("Настройка", new Color(149, 165, 166)),
|
||||||
|
Map.entry("Супер-Админ", new Color(113, 54, 138))
|
||||||
|
);
|
||||||
|
|
||||||
|
DESCRIPTIONS = Map.ofEntries(
|
||||||
|
Map.entry("Общие", "Основные команды бота"),
|
||||||
|
Map.entry("Роли", "Управление кастомными ролями"),
|
||||||
|
Map.entry("Развлечения", "Игры, мемы, рейтинги, текст"),
|
||||||
|
Map.entry(CommandCategories.GIF_INTERACTIVE, "Действия с участником в GIF (nekos.life)"),
|
||||||
|
Map.entry("Модерация", "Команды модерации сервера"),
|
||||||
|
Map.entry("Статистика", "Уровни, репутация и статистика"),
|
||||||
|
Map.entry("Утилиты", "Полезные инструменты"),
|
||||||
|
Map.entry("Система", "Метрики и служебная информация для админов"),
|
||||||
|
Map.entry("Настройка", "Настройка систем бота (только админы)"),
|
||||||
|
Map.entry("Супер-Админ", "Сервисные команды владельца бота")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HelpCategories() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getEmoji(String category) {
|
||||||
|
return EMOJIS.getOrDefault(category, "📌");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Color getColor(String category) {
|
||||||
|
return COLORS.getOrDefault(category, new Color(88, 101, 242));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDescription(String category) {
|
||||||
|
return DESCRIPTIONS.getOrDefault(category, "Команды категории");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCategoryVisible(String category, boolean isAdmin, boolean isSuper) {
|
||||||
|
if ("Настройка".equals(category) && !isAdmin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !"Супер-Админ".equals(category) || isSuper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
package creator.loki.feature.admin.commands;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.config.BotConfig;
|
||||||
|
import creator.loki.feature.rating.repository.RatingsRepository;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Команда для генерации всех rating значений для всех участников сервера
|
||||||
|
* Доступна только супер-админу
|
||||||
|
*/
|
||||||
|
public class GenerateRatingsCommand implements Command {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(GenerateRatingsCommand.class);
|
||||||
|
private final RatingsRepository ratingsRepository;
|
||||||
|
private final BotConfig config;
|
||||||
|
private final Random random = new Random();
|
||||||
|
|
||||||
|
public GenerateRatingsCommand(RatingsRepository ratingsRepository, BotConfig config) {
|
||||||
|
this.ratingsRepository = ratingsRepository;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "generateratings";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Генерирует все rating значения для всех участников сервера (только для супер-админа)";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "generateratings";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Супер-Админ";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
if (!config.getSuperAdminId().equals(context.getAuthor().getUser().getId())) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("❌ Недостаточно прав")
|
||||||
|
.withDescription("Эта команда доступна только супер-администратору бота.")
|
||||||
|
.withColor(Color.RED)
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.getGuild() == null) {
|
||||||
|
context.getChannel().sendMessage("❌ Команда работает только на сервере.").queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var initial = context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("⏳ Генерация рейтингов...")
|
||||||
|
.withDescription("Начинаю генерацию всех rating значений для участников сервера...")
|
||||||
|
.withColor(Color.YELLOW)
|
||||||
|
.build()
|
||||||
|
).complete();
|
||||||
|
|
||||||
|
try {
|
||||||
|
String guildId = context.getGuild().getId();
|
||||||
|
int memberCount = 0;
|
||||||
|
int generatedCount = 0;
|
||||||
|
|
||||||
|
for (Member member : context.getGuild().getMembers()) {
|
||||||
|
if (member.getUser().isBot()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
memberCount++;
|
||||||
|
String userId = member.getUser().getId();
|
||||||
|
|
||||||
|
generateRatingForUser(guildId, userId, "ship");
|
||||||
|
generateRatingForUser(guildId, userId, "gay");
|
||||||
|
generateRatingForUser(guildId, userId, "iq");
|
||||||
|
generateRatingForUser(guildId, userId, "pp");
|
||||||
|
generateRatingForUser(guildId, userId, "mustache");
|
||||||
|
generateRatingForUser(guildId, userId, "fat");
|
||||||
|
generateRatingForUser(guildId, userId, "boykisser");
|
||||||
|
|
||||||
|
generatedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
initial.editMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("✅ Генерация завершена")
|
||||||
|
.withDescription(
|
||||||
|
"Успешно сгенерированы все rating значения!\n\n" +
|
||||||
|
"**Статистика:**\n" +
|
||||||
|
"• Обработано участников: **" + memberCount + "**\n" +
|
||||||
|
"• Сгенерировано значений: **" + (generatedCount * 7) + "**\n" +
|
||||||
|
"• Типы рейтингов: ship, gay, iq, pp, mustache, fat, boykisser"
|
||||||
|
)
|
||||||
|
.withColor(Color.GREEN)
|
||||||
|
.withFooter("Инициатор: " + context.getAuthor().getUser().getName(), context.getAuthor().getAvatarUrl())
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
|
||||||
|
logger.info("Супер-админ {} сгенерировал рейтинги для {} участников",
|
||||||
|
context.getAuthor().getUser().getName(), memberCount);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Ошибка при генерации рейтингов", e);
|
||||||
|
initial.editMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("❌ Ошибка")
|
||||||
|
.withDescription("Произошла ошибка при генерации рейтингов: " + e.getMessage())
|
||||||
|
.withColor(Color.RED)
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерирует rating значение для пользователя
|
||||||
|
*/
|
||||||
|
private void generateRatingForUser(String guildId, String userId, String ratingType) {
|
||||||
|
// Проверяем существует ли уже значение
|
||||||
|
if (ratingsRepository.hasRating(guildId, userId, ratingType)) {
|
||||||
|
return; // Не перезаписываем существующие значения
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем значение в зависимости от типа
|
||||||
|
int value = switch (ratingType) {
|
||||||
|
case "iq" -> random.nextInt(201); // 0-200
|
||||||
|
case "pp" -> random.nextInt(31); // 0-30 см
|
||||||
|
case "mustache" -> {
|
||||||
|
// Редкий шанс на легендарные усы
|
||||||
|
if (random.nextInt(100) < 1) {
|
||||||
|
yield 1000 + random.nextInt(500); // 1000-1500
|
||||||
|
}
|
||||||
|
yield random.nextInt(101); // 0-100
|
||||||
|
}
|
||||||
|
case "fat" -> random.nextInt(101); // 0-100%
|
||||||
|
case "boykisser" -> random.nextInt(101); // 0-100%
|
||||||
|
default -> random.nextInt(101); // ship, gay: 0-100%
|
||||||
|
};
|
||||||
|
|
||||||
|
// Сохраняем значение в БД
|
||||||
|
ratingsRepository.setRating(guildId, userId, ratingType, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package creator.loki.database;
|
package creator.loki.feature.config.repository;
|
||||||
|
|
||||||
|
import creator.loki.database.Database;
|
||||||
|
import creator.loki.feature.config.repository.StringConfigStore;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -10,7 +12,7 @@ import java.sql.SQLException;
|
||||||
/**
|
/**
|
||||||
* Репозиторий для работы с конфигурацией
|
* Репозиторий для работы с конфигурацией
|
||||||
*/
|
*/
|
||||||
public class ConfigRepository {
|
public class ConfigRepository implements StringConfigStore {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ConfigRepository.class);
|
private static final Logger logger = LoggerFactory.getLogger(ConfigRepository.class);
|
||||||
private final Database database;
|
private final Database database;
|
||||||
|
|
||||||
|
|
@ -25,7 +27,7 @@ public class ConfigRepository {
|
||||||
try (PreparedStatement stmt = database.prepare("SELECT value FROM config WHERE key = ?")) {
|
try (PreparedStatement stmt = database.prepare("SELECT value FROM config WHERE key = ?")) {
|
||||||
stmt.setString(1, key);
|
stmt.setString(1, key);
|
||||||
ResultSet rs = stmt.executeQuery();
|
ResultSet rs = stmt.executeQuery();
|
||||||
|
|
||||||
if (rs.next()) {
|
if (rs.next()) {
|
||||||
return rs.getString("value");
|
return rs.getString("value");
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package creator.loki.feature.config.repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ключи конфигурации с привязкой к гильдии (хранение: {@code guildId + ":" + key} в общей таблице config).
|
||||||
|
*/
|
||||||
|
public interface GuildConfigRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return значение или {@code null}, если нет записи
|
||||||
|
*/
|
||||||
|
String get(String guildId, String key);
|
||||||
|
|
||||||
|
void set(String guildId, String key, String value);
|
||||||
|
|
||||||
|
void delete(String guildId, String key);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package creator.loki.feature.config.repository;
|
||||||
|
|
||||||
|
import creator.loki.feature.config.repository.GuildConfigRepository;
|
||||||
|
import creator.loki.feature.config.repository.StringConfigStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Реализация {@link GuildConfigRepository} поверх {@link StringConfigStore} с префиксом {@code guildId:}.
|
||||||
|
*/
|
||||||
|
public final class PrefixedGuildConfigRepository implements GuildConfigRepository {
|
||||||
|
|
||||||
|
private final StringConfigStore store;
|
||||||
|
|
||||||
|
public PrefixedGuildConfigRepository(StringConfigStore store) {
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String storageKey(String guildId, String key) {
|
||||||
|
return guildId + ":" + key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String get(String guildId, String key) {
|
||||||
|
return store.get(storageKey(guildId, key));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void set(String guildId, String key, String value) {
|
||||||
|
store.set(storageKey(guildId, key), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String guildId, String key) {
|
||||||
|
store.delete(storageKey(guildId, key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package creator.loki.feature.config.repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Плоское хранилище строковых ключей (глобальная таблица config и т.п.).
|
||||||
|
*/
|
||||||
|
public interface StringConfigStore {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return значение или {@code null}
|
||||||
|
*/
|
||||||
|
String get(String key);
|
||||||
|
|
||||||
|
void set(String key, String value);
|
||||||
|
|
||||||
|
void delete(String key);
|
||||||
|
}
|
||||||
|
|
@ -1,226 +1,110 @@
|
||||||
package creator.loki.feature.counter;
|
package creator.loki.feature.counter;
|
||||||
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
import creator.loki.data.JsonDataStore;
|
import creator.loki.feature.counter.repository.CounterStatsRepository;
|
||||||
import creator.loki.database.ConfigRepository;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
import java.util.*;
|
||||||
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 CounterStatsRepository statsRepo;
|
||||||
public class CounterManager {
|
private final ConfigRepository configRepo;
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CounterManager.class);
|
|
||||||
private final JsonDataStore dataStore;
|
private String counterChannelId;
|
||||||
private final ConfigRepository configRepo;
|
private String format = "【👥】Участники: {count}";
|
||||||
|
|
||||||
private String counterChannelId;
|
public CounterManager(CounterStatsRepository statsRepo, ConfigRepository configRepo) {
|
||||||
private String format = "【👥】Участники: {count}";
|
this.statsRepo = statsRepo;
|
||||||
|
this.configRepo = configRepo;
|
||||||
public CounterManager(JsonDataStore dataStore, ConfigRepository configRepo) {
|
loadConfig();
|
||||||
this.dataStore = dataStore;
|
}
|
||||||
this.configRepo = configRepo;
|
|
||||||
loadConfig();
|
/**
|
||||||
}
|
* Загрузить конфигурацию
|
||||||
|
*/
|
||||||
/**
|
private void loadConfig() {
|
||||||
* Загрузить конфигурацию
|
this.counterChannelId = configRepo.get("counter.channel_id");
|
||||||
*/
|
String savedFormat = configRepo.get("counter.format");
|
||||||
private void loadConfig() {
|
if (savedFormat != null) {
|
||||||
this.counterChannelId = configRepo.get("counter.channel_id");
|
this.format = savedFormat;
|
||||||
String savedFormat = configRepo.get("counter.format");
|
}
|
||||||
if (savedFormat != null) {
|
}
|
||||||
this.format = savedFormat;
|
|
||||||
}
|
/**
|
||||||
}
|
* Получить текущую дневную статистику
|
||||||
|
*/
|
||||||
/**
|
public DailyStats getDailyStats() {
|
||||||
* Получить данные счетчика
|
return statsRepo.getTodayStats();
|
||||||
*/
|
}
|
||||||
@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 saveDailyStats(DailyStats stats) {
|
||||||
}
|
statsRepo.saveStats(stats);
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Сохранить данные счетчика
|
/**
|
||||||
*/
|
* Зарегистрировать присоединение участника
|
||||||
private void saveCounterData(Map<String, Object> data) {
|
*/
|
||||||
dataStore.setData("counter_data", data);
|
public void recordJoin() {
|
||||||
}
|
DailyStats stats = getDailyStats();
|
||||||
|
stats.incrementJoined();
|
||||||
/**
|
saveDailyStats(stats);
|
||||||
* Установить канал счетчика
|
logger.debug("Зарегистрировано присоединение. Всего за день: {}", stats.getJoined());
|
||||||
*/
|
}
|
||||||
public void setCounterChannel(String channelId) {
|
|
||||||
configRepo.set("counter.channel_id", channelId);
|
/**
|
||||||
this.counterChannelId = channelId;
|
* Зарегистрировать выход участника
|
||||||
logger.info("Установлен канал счетчика: {}", channelId);
|
*/
|
||||||
}
|
public void recordLeave() {
|
||||||
|
DailyStats stats = getDailyStats();
|
||||||
/**
|
stats.incrementLeft();
|
||||||
* Установить формат отображения
|
saveDailyStats(stats);
|
||||||
*/
|
logger.debug("Зарегистрирован выход. Всего за день: {}", stats.getLeft());
|
||||||
public void setFormat(String format) {
|
}
|
||||||
configRepo.set("counter.format", format);
|
|
||||||
this.format = format;
|
/**
|
||||||
logger.info("Установлен формат: {}", format);
|
* Обновить счетчик участников
|
||||||
}
|
*/
|
||||||
|
public void updateCounter(Guild guild) {
|
||||||
/**
|
if (counterChannelId == null || counterChannelId.isEmpty()) {
|
||||||
* Получить текущую дневную статистику
|
return;
|
||||||
*/
|
}
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public DailyStats getDailyStats() {
|
VoiceChannel channel = guild.getVoiceChannelById(counterChannelId);
|
||||||
Map<String, Object> data = getCounterData();
|
if (channel == null) {
|
||||||
Map<String, Object> statsMap = (Map<String, Object>) data.get("daily_stats");
|
logger.warn("Канал счетчика не найден: {}", counterChannelId);
|
||||||
|
return;
|
||||||
if (statsMap == null) {
|
}
|
||||||
return new DailyStats();
|
|
||||||
}
|
int memberCount = guild.getMemberCount();
|
||||||
|
String newName = format.replace("{count}", String.valueOf(memberCount));
|
||||||
DailyStats stats = new DailyStats();
|
|
||||||
stats.setDate((String) statsMap.getOrDefault("date", LocalDate.now().toString()));
|
// Обновляем название канала только если оно изменилось
|
||||||
stats.setJoined(((Number) statsMap.getOrDefault("joined", 0)).intValue());
|
if (!channel.getName().equals(newName)) {
|
||||||
stats.setLeft(((Number) statsMap.getOrDefault("left", 0)).intValue());
|
channel.getManager().setName(newName).queue(
|
||||||
|
success -> logger.debug("Обновлен счетчик участников: {}", memberCount),
|
||||||
// Если статистика не сегодняшняя, создаем новую
|
error -> logger.error("Ошибка при обновлении счетчика", error)
|
||||||
if (!stats.isToday()) {
|
);
|
||||||
// Сохраняем старую в историю
|
}
|
||||||
saveToHistory(stats);
|
}
|
||||||
stats = new DailyStats();
|
|
||||||
}
|
/**
|
||||||
|
* Получить историю статистики
|
||||||
return stats;
|
*/
|
||||||
}
|
public List<DailyStats> getHistory(int days) {
|
||||||
|
return statsRepo.getHistory(days);
|
||||||
/**
|
}
|
||||||
* Сохранить статистику
|
|
||||||
*/
|
// Getters
|
||||||
private void saveDailyStats(DailyStats stats) {
|
public String getCounterChannelId() { return counterChannelId; }
|
||||||
Map<String, Object> data = getCounterData();
|
public String getFormat() { return format; }
|
||||||
|
}
|
||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package creator.loki.feature.counter;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Provides;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import creator.loki.database.Database;
|
||||||
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
|
import creator.loki.feature.counter.repository.CounterStatsRepository;
|
||||||
|
|
||||||
|
public class CounterModule extends AbstractModule {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
CounterStatsRepository provideCounterStatsRepository(Database database) {
|
||||||
|
return new CounterStatsRepository(database);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
CounterManager provideCounterManager(CounterStatsRepository statsRepo, ConfigRepository configRepo) {
|
||||||
|
return new CounterManager(statsRepo, configRepo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,159 +0,0 @@
|
||||||
package creator.loki.feature.counter;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
package creator.loki.feature.counter;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import creator.loki.util.GuildChannelResolve;
|
||||||
|
import net.dv8tion.jda.api.Permission;
|
||||||
|
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix команда настройки счетчика участников
|
||||||
|
*/
|
||||||
|
public class CounterSetupPrefixCommand implements Command {
|
||||||
|
private final CounterManager counterManager;
|
||||||
|
private final ConfigRepository configRepo;
|
||||||
|
|
||||||
|
public CounterSetupPrefixCommand(CounterManager counterManager, ConfigRepository configRepo) {
|
||||||
|
this.counterManager = counterManager;
|
||||||
|
this.configRepo = configRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "counter-setup";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Настройка счетчика участников";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "counter-setup <channel|format|info> [значение]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Настройка";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
// Проверка админ прав
|
||||||
|
if (!context.getAuthor().hasPermission(Permission.ADMINISTRATOR)) {
|
||||||
|
context.getChannel().sendMessage("❌ У вас нет прав администратора!").queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> args = context.getArgs();
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
sendHelp(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String action = args.get(0).toLowerCase();
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "channel" -> handleChannel(context, args);
|
||||||
|
case "format" -> handleFormat(context, args);
|
||||||
|
case "info" -> handleInfo(context);
|
||||||
|
default -> sendHelp(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleChannel(CommandContext context, List<String> args) {
|
||||||
|
if (args.size() < 2) {
|
||||||
|
context.getChannel().sendMessage(
|
||||||
|
"❌ Укажите голосовой канал: `#канал`, `<#id>` или числовой id. Пример: `!counter-setup channel 123456789012345678`"
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
VoiceChannel channel = GuildChannelResolve.voice(context.getGuild(), context.getMessage(), args, 1);
|
||||||
|
if (channel == null) {
|
||||||
|
context.getChannel().sendMessage(
|
||||||
|
"❌ Не найден голосовой канал. Укажите `#войс`, `<#id>` или id из 17-20 цифр."
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
configRepo.set("counter.channel_id", channel.getId());
|
||||||
|
|
||||||
|
// Сразу обновляем название канала
|
||||||
|
int memberCount = context.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();
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("✅ Канал счетчика установлен")
|
||||||
|
.withDescription("Канал " + channel.getAsMention() + " теперь отображает количество участников")
|
||||||
|
.addField("Текущее количество", String.valueOf(memberCount), false)
|
||||||
|
.withColor(Color.GREEN)
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleFormat(CommandContext context, List<String> args) {
|
||||||
|
if (args.size() < 2) {
|
||||||
|
context.getChannel().sendMessage("❌ Укажите формат! Пример: `!counter-setup format 【👥】Участники: {count}`").queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Объединяем все аргументы в одну строку
|
||||||
|
String format = String.join(" ", args.subList(1, args.size()));
|
||||||
|
|
||||||
|
if (!format.contains("{count}")) {
|
||||||
|
context.getChannel().sendMessage("❌ Формат должен содержать `{count}`!").queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
configRepo.set("counter.format", format);
|
||||||
|
|
||||||
|
// Обновляем канал если он установлен
|
||||||
|
String channelId = configRepo.get("counter.channel_id");
|
||||||
|
if (channelId != null) {
|
||||||
|
VoiceChannel channel = context.getGuild().getVoiceChannelById(channelId);
|
||||||
|
if (channel != null) {
|
||||||
|
int memberCount = context.getGuild().getMemberCount();
|
||||||
|
String newName = format.replace("{count}", String.valueOf(memberCount));
|
||||||
|
channel.getManager().setName(newName).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("✅ Формат обновлен")
|
||||||
|
.withDescription("Новый формат: `" + format + "`")
|
||||||
|
.addField("Пример", format.replace("{count}", "123"), false)
|
||||||
|
.withColor(Color.GREEN)
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleInfo(CommandContext context) {
|
||||||
|
String channelId = configRepo.get("counter.channel_id");
|
||||||
|
String format = configRepo.get("counter.format");
|
||||||
|
|
||||||
|
var embed = EmbedFactory.create()
|
||||||
|
.withTitle("📊 Конфигурация счетчика участников")
|
||||||
|
.withColor(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(context.getGuild().getMemberCount())), false);
|
||||||
|
} else {
|
||||||
|
embed.addField("Формат", "❌ Не установлен", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(embed.build()).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendHelp(CommandContext context) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("📊 Настройка счетчика участников")
|
||||||
|
.withDescription("""
|
||||||
|
**Доступные команды:**
|
||||||
|
• `!counter-setup channel #войс` или `id` - канал счётчика
|
||||||
|
• `!counter-setup format 【👥】Участники: {count}` - Установить формат
|
||||||
|
• `!counter-setup info` - Показать текущую конфигурацию
|
||||||
|
|
||||||
|
В формате используйте `{count}` для отображения количества.
|
||||||
|
Только для администраторов!
|
||||||
|
""")
|
||||||
|
.withColor(Color.BLUE)
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,66 +1,66 @@
|
||||||
package creator.loki.feature.counter;
|
package creator.loki.feature.counter;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Дневная статистика участников
|
* Дневная статистика участников
|
||||||
*/
|
*/
|
||||||
public class DailyStats {
|
public class DailyStats {
|
||||||
private String date;
|
private String date;
|
||||||
private int joined;
|
private int joined;
|
||||||
private int left;
|
private int left;
|
||||||
private int difference;
|
private int difference;
|
||||||
|
|
||||||
public DailyStats() {
|
public DailyStats() {
|
||||||
this.date = LocalDate.now().toString();
|
this.date = LocalDate.now().toString();
|
||||||
this.joined = 0;
|
this.joined = 0;
|
||||||
this.left = 0;
|
this.left = 0;
|
||||||
this.difference = 0;
|
this.difference = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DailyStats(String date, int joined, int left) {
|
public DailyStats(String date, int joined, int left) {
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.joined = joined;
|
this.joined = joined;
|
||||||
this.left = left;
|
this.left = left;
|
||||||
this.difference = joined - left;
|
this.difference = joined - left;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
public String getDate() { return date; }
|
public String getDate() { return date; }
|
||||||
public int getJoined() { return joined; }
|
public int getJoined() { return joined; }
|
||||||
public int getLeft() { return left; }
|
public int getLeft() { return left; }
|
||||||
public int getDifference() { return difference; }
|
public int getDifference() { return difference; }
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
public void setDate(String date) { this.date = date; }
|
public void setDate(String date) { this.date = date; }
|
||||||
public void setJoined(int joined) {
|
public void setJoined(int joined) {
|
||||||
this.joined = joined;
|
this.joined = joined;
|
||||||
updateDifference();
|
updateDifference();
|
||||||
}
|
}
|
||||||
public void setLeft(int left) {
|
public void setLeft(int left) {
|
||||||
this.left = left;
|
this.left = left;
|
||||||
updateDifference();
|
updateDifference();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Методы
|
// Методы
|
||||||
public void incrementJoined() {
|
public void incrementJoined() {
|
||||||
this.joined++;
|
this.joined++;
|
||||||
updateDifference();
|
updateDifference();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void incrementLeft() {
|
public void incrementLeft() {
|
||||||
this.left++;
|
this.left++;
|
||||||
updateDifference();
|
updateDifference();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateDifference() {
|
private void updateDifference() {
|
||||||
this.difference = this.joined - this.left;
|
this.difference = this.joined - this.left;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверить является ли статистика сегодняшней
|
* Проверить является ли статистика сегодняшней
|
||||||
*/
|
*/
|
||||||
public boolean isToday() {
|
public boolean isToday() {
|
||||||
return this.date.equals(LocalDate.now().toString());
|
return this.date.equals(LocalDate.now().toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,86 +1,86 @@
|
||||||
package creator.loki.feature.counter;
|
package creator.loki.feature.counter;
|
||||||
|
|
||||||
import creator.loki.feature.counter.CounterManager;
|
import creator.loki.feature.counter.CounterManager;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
|
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
|
||||||
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
|
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
|
||||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener для счетчика участников
|
* Listener для счетчика участников
|
||||||
*/
|
*/
|
||||||
public class MemberCounterListener extends ListenerAdapter {
|
public class MemberCounterListener extends ListenerAdapter {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MemberCounterListener.class);
|
private static final Logger logger = LoggerFactory.getLogger(MemberCounterListener.class);
|
||||||
private final CounterManager counterManager;
|
private final CounterManager counterManager;
|
||||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||||
private Guild guild;
|
private Guild guild;
|
||||||
|
|
||||||
public MemberCounterListener(CounterManager counterManager) {
|
public MemberCounterListener(CounterManager counterManager) {
|
||||||
this.counterManager = counterManager;
|
this.counterManager = counterManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Запустить периодическое обновление счетчика
|
* Запустить периодическое обновление счетчика
|
||||||
*/
|
*/
|
||||||
public void startScheduler(Guild guild) {
|
public void startScheduler(Guild guild) {
|
||||||
this.guild = guild;
|
this.guild = guild;
|
||||||
|
|
||||||
// Обновляем счетчик каждые 5 минут
|
// Обновляем счетчик каждые 5 минут
|
||||||
scheduler.scheduleAtFixedRate(() -> {
|
scheduler.scheduleAtFixedRate(() -> Thread.startVirtualThread(() -> {
|
||||||
try {
|
try {
|
||||||
counterManager.updateCounter(guild);
|
counterManager.updateCounter(guild);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Ошибка при обновлении счетчика", e);
|
logger.error("Ошибка при обновлении счетчика", e);
|
||||||
}
|
}
|
||||||
}, 0, 5, TimeUnit.MINUTES);
|
}), 0, 5, TimeUnit.MINUTES);
|
||||||
|
|
||||||
logger.info("Запущен планировщик обновления счетчика участников");
|
logger.info("Запущен планировщик обновления счетчика участников");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onGuildMemberJoin(GuildMemberJoinEvent event) {
|
public void onGuildMemberJoin(GuildMemberJoinEvent event) {
|
||||||
// Игнорируем ботов
|
// Игнорируем ботов
|
||||||
if (event.getUser().isBot()) {
|
if (event.getUser().isBot()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
counterManager.recordJoin();
|
counterManager.recordJoin();
|
||||||
counterManager.updateCounter(event.getGuild());
|
counterManager.updateCounter(event.getGuild());
|
||||||
|
|
||||||
logger.info("Участник {} присоединился к серверу", event.getUser().getName());
|
logger.info("Участник {} присоединился к серверу", event.getUser().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onGuildMemberRemove(GuildMemberRemoveEvent event) {
|
public void onGuildMemberRemove(GuildMemberRemoveEvent event) {
|
||||||
// Игнорируем ботов
|
// Игнорируем ботов
|
||||||
if (event.getUser().isBot()) {
|
if (event.getUser().isBot()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
counterManager.recordLeave();
|
counterManager.recordLeave();
|
||||||
counterManager.updateCounter(event.getGuild());
|
counterManager.updateCounter(event.getGuild());
|
||||||
|
|
||||||
logger.info("Участник {} покинул сервер", event.getUser().getName());
|
logger.info("Участник {} покинул сервер", event.getUser().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Остановить планировщик
|
* Остановить планировщик
|
||||||
*/
|
*/
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
scheduler.shutdown();
|
scheduler.shutdown();
|
||||||
try {
|
try {
|
||||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
scheduler.shutdownNow();
|
scheduler.shutdownNow();
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
scheduler.shutdownNow();
|
scheduler.shutdownNow();
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
package creator.loki.feature.counter.repository;
|
||||||
|
|
||||||
|
import creator.loki.database.Database;
|
||||||
|
import creator.loki.feature.counter.DailyStats;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Репозиторий для работы со статистикой счетчика участников
|
||||||
|
*/
|
||||||
|
public class CounterStatsRepository {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CounterStatsRepository.class);
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
public CounterStatsRepository(Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить статистику за дату
|
||||||
|
*/
|
||||||
|
public DailyStats getStats(String date) {
|
||||||
|
String sql = "SELECT * FROM counter_stats WHERE date = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, date);
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
DailyStats stats = new DailyStats();
|
||||||
|
stats.setDate(rs.getString("date"));
|
||||||
|
stats.setJoined(rs.getInt("joined"));
|
||||||
|
stats.setLeft(rs.getInt("left_count"));
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка получения статистики за {}", date, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить статистику за сегодня
|
||||||
|
*/
|
||||||
|
public DailyStats getTodayStats() {
|
||||||
|
String today = LocalDate.now().toString();
|
||||||
|
DailyStats stats = getStats(today);
|
||||||
|
|
||||||
|
if (stats == null) {
|
||||||
|
stats = new DailyStats();
|
||||||
|
stats.setDate(today);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить статистику
|
||||||
|
*/
|
||||||
|
public void saveStats(DailyStats stats) {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO counter_stats (date, joined, left_count, difference)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT(date)
|
||||||
|
DO UPDATE SET
|
||||||
|
joined = excluded.joined,
|
||||||
|
left_count = excluded.left_count,
|
||||||
|
difference = excluded.difference
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, stats.getDate());
|
||||||
|
stmt.setInt(2, stats.getJoined());
|
||||||
|
stmt.setInt(3, stats.getLeft());
|
||||||
|
stmt.setInt(4, stats.getDifference());
|
||||||
|
stmt.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка сохранения статистики за {}", stats.getDate(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить историю за последние N дней
|
||||||
|
*/
|
||||||
|
public List<DailyStats> getHistory(int days) {
|
||||||
|
List<DailyStats> history = new ArrayList<>();
|
||||||
|
String sql = "SELECT * FROM counter_stats ORDER BY date DESC LIMIT ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setInt(1, days);
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
DailyStats stats = new DailyStats();
|
||||||
|
stats.setDate(rs.getString("date"));
|
||||||
|
stats.setJoined(rs.getInt("joined"));
|
||||||
|
stats.setLeft(rs.getInt("left_count"));
|
||||||
|
history.add(stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка получения истории", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.Collections.reverse(history);
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удалить старую статистику (старше N дней)
|
||||||
|
*/
|
||||||
|
public void cleanOldStats(int daysToKeep) {
|
||||||
|
String sql = """
|
||||||
|
DELETE FROM counter_stats
|
||||||
|
WHERE date < date('now', '-' || ? || ' days')
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setInt(1, daysToKeep);
|
||||||
|
int deleted = stmt.executeUpdate();
|
||||||
|
if (deleted > 0) {
|
||||||
|
logger.info("Удалено {} старых записей статистики", deleted);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка очистки старой статистики", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package creator.loki.feature.economy;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
|
import creator.loki.core.registry.CommandRegistry;
|
||||||
|
import creator.loki.core.security.CooldownManager;
|
||||||
|
import creator.loki.feature.economy.command.bank.BankCommand;
|
||||||
|
import creator.loki.feature.economy.command.core.BalanceCommand;
|
||||||
|
import creator.loki.feature.economy.command.core.BaltopCommand;
|
||||||
|
import creator.loki.feature.economy.command.core.DailyCommand;
|
||||||
|
import creator.loki.feature.economy.command.core.PayCommand;
|
||||||
|
import creator.loki.feature.economy.command.minigame.BlackjackCommand;
|
||||||
|
import creator.loki.feature.economy.command.minigame.FishCommand;
|
||||||
|
import creator.loki.feature.economy.command.minigame.JobCommand;
|
||||||
|
import creator.loki.feature.economy.command.minigame.SlotCommand;
|
||||||
|
import creator.loki.feature.economy.command.minigame.WorkCommand;
|
||||||
|
import creator.loki.feature.economy.command.social.RobCommand;
|
||||||
|
import creator.loki.feature.economy.service.BlackjackService;
|
||||||
|
import creator.loki.feature.economy.service.EconomyService;
|
||||||
|
import creator.loki.feature.economy.service.FishingService;
|
||||||
|
import creator.loki.feature.economy.service.JobService;
|
||||||
|
import creator.loki.feature.economy.service.SlotService;
|
||||||
|
|
||||||
|
public class EconomyCommandRegistrar implements CommandRegistrar {
|
||||||
|
private final EconomyService economyService;
|
||||||
|
private final FishingService fishingService;
|
||||||
|
private final BlackjackService blackjackService;
|
||||||
|
private final SlotService slotService;
|
||||||
|
private final JobService jobService;
|
||||||
|
private final CooldownManager cooldownManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public EconomyCommandRegistrar(
|
||||||
|
EconomyService economyService,
|
||||||
|
FishingService fishingService,
|
||||||
|
BlackjackService blackjackService,
|
||||||
|
SlotService slotService,
|
||||||
|
JobService jobService,
|
||||||
|
CooldownManager cooldownManager
|
||||||
|
) {
|
||||||
|
this.economyService = economyService;
|
||||||
|
this.fishingService = fishingService;
|
||||||
|
this.blackjackService = blackjackService;
|
||||||
|
this.slotService = slotService;
|
||||||
|
this.jobService = jobService;
|
||||||
|
this.cooldownManager = cooldownManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void register(CommandRegistry registry) {
|
||||||
|
registry.register(new BalanceCommand(economyService));
|
||||||
|
registry.register(new BaltopCommand(economyService));
|
||||||
|
registry.register(new PayCommand(economyService));
|
||||||
|
registry.register(new DailyCommand(economyService));
|
||||||
|
registry.register(new BankCommand(economyService));
|
||||||
|
registry.register(new RobCommand(economyService, cooldownManager));
|
||||||
|
registry.register(new FishCommand(fishingService));
|
||||||
|
registry.register(new BlackjackCommand(blackjackService, economyService));
|
||||||
|
registry.register(new SlotCommand(slotService, economyService));
|
||||||
|
registry.register(new JobCommand(jobService));
|
||||||
|
registry.register(new WorkCommand(jobService));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package creator.loki.feature.economy;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Provides;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import creator.loki.core.security.CooldownManager;
|
||||||
|
import creator.loki.database.Database;
|
||||||
|
import creator.loki.feature.economy.repository.EconomyRepository;
|
||||||
|
import creator.loki.feature.economy.repository.SqlEconomyRepository;
|
||||||
|
import creator.loki.feature.economy.service.BlackjackService;
|
||||||
|
import creator.loki.feature.economy.service.EconomyService;
|
||||||
|
import creator.loki.feature.economy.service.FishingService;
|
||||||
|
import creator.loki.feature.economy.service.JobService;
|
||||||
|
import creator.loki.feature.economy.service.SlotService;
|
||||||
|
|
||||||
|
public class EconomyModule extends AbstractModule {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
EconomyRepository provideEconomyRepository(Database database) {
|
||||||
|
return new SqlEconomyRepository(database);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
EconomyService provideEconomyService(EconomyRepository repository, CooldownManager cooldownManager) {
|
||||||
|
return new EconomyService(repository, cooldownManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
FishingService provideFishingService(EconomyRepository repository) {
|
||||||
|
return new FishingService(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
BlackjackService provideBlackjackService(EconomyRepository repository) {
|
||||||
|
return new BlackjackService(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
SlotService provideSlotService(EconomyRepository repository) {
|
||||||
|
return new SlotService(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
JobService provideJobService(EconomyRepository repository) {
|
||||||
|
return new JobService(repository);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package creator.loki.feature.economy.command.bank;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||||
|
import creator.loki.feature.economy.service.EconomyService;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BankCommand implements Command {
|
||||||
|
private final EconomyService economyService;
|
||||||
|
|
||||||
|
public BankCommand(EconomyService economyService) {
|
||||||
|
this.economyService = economyService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "bank"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() { return "Управление банковским счётом"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() { return "bank [deposit|withdraw|balance] <amount>"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() { return "Экономика"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext ctx) {
|
||||||
|
List<String> args = ctx.getArgs();
|
||||||
|
String uid = ctx.getAuthor().getId();
|
||||||
|
if (args.isEmpty() || args.get(0).equalsIgnoreCase("balance")) {
|
||||||
|
EconomyAccount acc = economyService.getAccount(uid);
|
||||||
|
ctx.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("🏦 Банк — " + ctx.getAuthor().getEffectiveName())
|
||||||
|
.withColor(new Color(52, 152, 219))
|
||||||
|
.addField("Наличные", "**" + acc.getCash() + "** LC", true)
|
||||||
|
.addField("Банк", "**" + acc.getBank() + "** LC", true)
|
||||||
|
.addField("Всего", "**" + acc.getTotalBalance() + "** LC", true)
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String sub = args.get(0).toLowerCase();
|
||||||
|
if (!sub.equals("deposit") && !sub.equals("withdraw")) {
|
||||||
|
ctx.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Неизвестная подкоманда. Используй: " + getUsage())
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.size() < 2) {
|
||||||
|
ctx.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Укажите сумму: bank " + sub + " <amount>")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long amount;
|
||||||
|
try {
|
||||||
|
amount = Long.parseLong(args.get(1));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
ctx.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Сумма должна быть числом!")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (amount <= 0) {
|
||||||
|
ctx.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Сумма должна быть положительной!")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EconomyAccount acc = economyService.getAccount(uid);
|
||||||
|
boolean isDep = sub.equals("deposit");
|
||||||
|
long balance = isDep ? acc.getCash() : acc.getBank();
|
||||||
|
if (balance < amount) {
|
||||||
|
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка",
|
||||||
|
"Недостаточно " + (isDep ? "наличных" : "в банке") + "! У вас **" + balance + "** LC")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isDep) economyService.deposit(uid, amount);
|
||||||
|
else economyService.withdraw(uid, amount);
|
||||||
|
ctx.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.success(isDep ? "Депозит" : "Снятие",
|
||||||
|
"Ты " + (isDep ? "положил" : "снял") + " **" + amount + "** LC " + (isDep ? "в банк!" : "из банка!"))
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package creator.loki.feature.economy.command.core;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||||
|
import creator.loki.feature.economy.service.EconomyService;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BalanceCommand implements Command {
|
||||||
|
private final EconomyService economyService;
|
||||||
|
|
||||||
|
public BalanceCommand(EconomyService economyService) {
|
||||||
|
this.economyService = economyService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "balance";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Показать баланс пользователя";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "balance [@user]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Экономика";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
List<Member> mentioned = context.getMessage().getMentions().getMembers();
|
||||||
|
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
|
||||||
|
|
||||||
|
EconomyAccount account = economyService.getAccount(target.getId());
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("💰 Баланс — " + target.getEffectiveName())
|
||||||
|
.withColor(new Color(255, 215, 0))
|
||||||
|
.addField("Наличные", "**" + account.getCash() + "** LC", true)
|
||||||
|
.addField("Банк", "**" + account.getBank() + "** LC", true)
|
||||||
|
.addField("Всего", "**" + account.getTotalBalance() + "** LC", true)
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package creator.loki.feature.economy.command.core;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||||
|
import creator.loki.feature.economy.service.EconomyService;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BaltopCommand implements Command {
|
||||||
|
private final EconomyService economyService;
|
||||||
|
|
||||||
|
public BaltopCommand(EconomyService economyService) {
|
||||||
|
this.economyService = economyService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "baltop";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Топ 10 богатейших пользователей";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Экономика";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
List<EconomyAccount> top = economyService.getTopBalances(10);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < top.size(); i++) {
|
||||||
|
EconomyAccount acc = top.get(i);
|
||||||
|
String name = context.getGuild().getMemberById(acc.getUserId()) != null
|
||||||
|
? context.getGuild().getMemberById(acc.getUserId()).getEffectiveName()
|
||||||
|
: acc.getUserId();
|
||||||
|
sb.append("**").append(i + 1).append(".** ")
|
||||||
|
.append(name)
|
||||||
|
.append(" — **").append(acc.getTotalBalance()).append("** LC\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("🏆 Топ богатейших")
|
||||||
|
.withDescription(sb.toString())
|
||||||
|
.withColor(new Color(255, 215, 0))
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package creator.loki.feature.economy.command.core;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.feature.economy.service.EconomyService;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
public class DailyCommand implements Command {
|
||||||
|
private final EconomyService economyService;
|
||||||
|
|
||||||
|
public DailyCommand(EconomyService economyService) {
|
||||||
|
this.economyService = economyService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "daily";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Получить ежедневную награду";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Экономика";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
EconomyService.DailyResult result = economyService.claimDaily(context.getAuthor().getId());
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.warning("Ежедневная награда",
|
||||||
|
"Ты уже получил награду сегодня! Возвращайся завтра.")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("🎁 Ежедневная награда")
|
||||||
|
.withDescription("Ты получил **" + result.amount() + "** LC!")
|
||||||
|
.withColor(new Color(46, 204, 113))
|
||||||
|
.addField("Стрик", "**" + result.streak() + "** дней", true)
|
||||||
|
.addField("Баланс", "**" + result.newCash() + "** LC", true)
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
package creator.loki.feature.economy.command.core;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.feature.economy.service.EconomyService;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PayCommand implements Command {
|
||||||
|
private final EconomyService economyService;
|
||||||
|
|
||||||
|
public PayCommand(EconomyService economyService) {
|
||||||
|
this.economyService = economyService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "pay";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Перевести деньги другому пользователю";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "pay <@user> <amount>";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Экономика";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
List<Member> mentioned = context.getMessage().getMentions().getMembers();
|
||||||
|
if (mentioned.isEmpty()) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Укажите пользователя: " + getUsage())
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Member target = mentioned.get(0);
|
||||||
|
if (target.getId().equals(context.getAuthor().getId())) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Нельзя переводить деньги самому себе!")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> args = context.getArgs();
|
||||||
|
if (args.size() < 2) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Укажите сумму: " + getUsage())
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long amount;
|
||||||
|
try {
|
||||||
|
amount = Long.parseLong(args.get(1));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Сумма должна быть числом!")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount <= 0) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Сумма должна быть положительной!")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var senderAccount = economyService.getAccount(context.getAuthor().getId());
|
||||||
|
if (senderAccount.getCash() < amount) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Недостаточно средств! У вас **" + senderAccount.getCash() + "** LC")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
economyService.transfer(context.getAuthor().getId(), target.getId(), amount);
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.success("Перевод выполнен",
|
||||||
|
context.getAuthor().getAsMention() + " перевёл **" + amount + "** LC " + target.getAsMention())
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
package creator.loki.feature.economy.command.minigame;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.feature.economy.service.BlackjackService;
|
||||||
|
import creator.loki.feature.economy.service.EconomyService;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
|
||||||
|
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class BlackjackCommand implements Command {
|
||||||
|
|
||||||
|
private final BlackjackService blackjackService;
|
||||||
|
private final EconomyService economyService;
|
||||||
|
private final Set<String> activeGames = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
public BlackjackCommand(BlackjackService blackjackService, EconomyService economyService) {
|
||||||
|
this.blackjackService = blackjackService;
|
||||||
|
this.economyService = economyService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "blackjack";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Блэкджек — играй против дилера";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "bj <ставка> — начать игру\nbj hit — взять карту\nbj stand — остановиться";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Экономика";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
var args = context.getArgs();
|
||||||
|
var userId = context.getAuthor().getId();
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.warning("Блэкджек", "Используй: `!bj <ставка>` чтобы начать")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var action = args.get(0).toLowerCase();
|
||||||
|
if (action.equals("hit")) {
|
||||||
|
handleAction(context, userId, true);
|
||||||
|
} else if (action.equals("stand")) {
|
||||||
|
handleAction(context, userId, false);
|
||||||
|
} else {
|
||||||
|
handleStart(context, userId, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleStart(CommandContext context, String userId, String betStr) {
|
||||||
|
if (activeGames.contains(userId)) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "У тебя уже есть активная игра! Используй `!bj hit` или `!bj stand`")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long bet;
|
||||||
|
try {
|
||||||
|
bet = Long.parseLong(betStr);
|
||||||
|
if (bet <= 0) throw new NumberFormatException();
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Укажи положительное число в качестве ставки")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var account = economyService.getAccount(userId);
|
||||||
|
if (account.getCash() < bet) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Недостаточно средств! Твой баланс: " + account.getCash() + " LC")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BlackjackService.BlackjackState state = blackjackService.startGame(userId, bet);
|
||||||
|
if (state == null) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Не удалось начать игру.")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.isOver()) activeGames.add(userId);
|
||||||
|
context.getChannel().sendMessageEmbeds(buildGameEmbed(state)).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleAction(CommandContext context, String userId, boolean isHit) {
|
||||||
|
if (!activeGames.contains(userId)) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Нет активной игры! Начни с `!bj <ставка>`")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BlackjackService.BlackjackState state = isHit
|
||||||
|
? blackjackService.hit(userId)
|
||||||
|
: blackjackService.stand(userId);
|
||||||
|
if (state.isOver()) activeGames.remove(userId);
|
||||||
|
context.getChannel().sendMessageEmbeds(buildGameEmbed(state)).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MessageEmbed buildGameEmbed(BlackjackService.BlackjackState state) {
|
||||||
|
var embed = EmbedFactory.create()
|
||||||
|
.withTitle("🃏 Блэкджек")
|
||||||
|
.withColor(new Color(34, 139, 34))
|
||||||
|
.addField("Твои карты", formatCards(state.playerCards(), state.playerSum()), true);
|
||||||
|
if (state.isOver()) {
|
||||||
|
embed.addField("Карты дилера", formatCards(state.dealerCards(), state.dealerSum()), true)
|
||||||
|
.addField("Результат", formatResult(state), false);
|
||||||
|
} else {
|
||||||
|
embed.addField("Карты дилера", formatHiddenDealerCards(state.dealerCards()), true)
|
||||||
|
.withFooter("!bj hit — взять карту | !bj stand — остановиться");
|
||||||
|
}
|
||||||
|
return embed.addField("Ставка", state.bet() + " LC", false).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCards(String[] cards, int sum) {
|
||||||
|
return String.join(" + ", cards) + " = " + sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatHiddenDealerCards(String[] cards) {
|
||||||
|
var sb = new StringBuilder("?");
|
||||||
|
for (int i = 1; i < cards.length; i++) sb.append(" + ").append(cards[i]);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatResult(BlackjackService.BlackjackState state) {
|
||||||
|
long payout = state.payout();
|
||||||
|
if (payout > state.bet()) return "🎉 Блэкджек! +" + payout + " LC";
|
||||||
|
if (payout > 0) return "✅ Ты выиграл +" + payout + " LC";
|
||||||
|
if (payout == state.bet()) return "🤝 Ничья. Возврат " + payout + " LC";
|
||||||
|
return "💔 Ты проиграл " + state.bet() + " LC";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package creator.loki.feature.economy.command.minigame;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.feature.economy.service.FishingService;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class FishCommand implements Command {
|
||||||
|
|
||||||
|
private static record FishSession(String difficulty, long startTime) {}
|
||||||
|
|
||||||
|
private final FishingService fishingService;
|
||||||
|
private final ConcurrentHashMap<String, FishSession> sessions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public FishCommand(FishingService fishingService) {
|
||||||
|
this.fishingService = fishingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "fish";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Рыбалка — угадай количество рыбы в пруду";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "fish <easy/normal/hard> — начать рыбалку\nfish <число> — ответить";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Экономика";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
var args = context.getArgs();
|
||||||
|
var userId = context.getAuthor().getId();
|
||||||
|
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.warning("Рыбалка", "Используй: `!fish easy/normal/hard`")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var first = args.get(0).toLowerCase();
|
||||||
|
|
||||||
|
if (first.equals("easy") || first.equals("normal") || first.equals("hard")) {
|
||||||
|
FishingService.FishResult result = fishingService.startFishing(userId, first);
|
||||||
|
sessions.put(userId, new FishSession(first, System.currentTimeMillis()));
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("🎣 Рыбалка")
|
||||||
|
.withDescription("```\n" + String.join("\n", result.grid()) + "\n```")
|
||||||
|
.withColor(new Color(0, 150, 255))
|
||||||
|
.addField("Вопрос", "Сколько **" + result.targetFish() + "** ты видишь?", false)
|
||||||
|
.withFooter("Напиши !fish <число>, чтобы ответить")
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int answer = Integer.parseInt(first);
|
||||||
|
FishSession session = sessions.remove(userId);
|
||||||
|
if (session == null) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Нет активной рыбалки! Начни с `!fish easy/normal/hard`")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FishingService.FishResult result = fishingService.checkAnswer(userId, answer, session.startTime(), session.difficulty());
|
||||||
|
var embed = EmbedFactory.create()
|
||||||
|
.withTitle("🎣 Рыбалка — Результат")
|
||||||
|
.withDescription("```\n" + String.join("\n", result.grid()) + "\n```")
|
||||||
|
.withColor(new Color(255, 215, 0));
|
||||||
|
|
||||||
|
if (result.reward() > 0) {
|
||||||
|
embed.addField("Твой ответ", String.valueOf(answer), true)
|
||||||
|
.addField("Правильный ответ", String.valueOf(result.count()), true)
|
||||||
|
.addField("Награда", "+" + result.reward() + " LC", true)
|
||||||
|
.addField("Новый баланс", result.cashAfter() + " LC", true);
|
||||||
|
} else {
|
||||||
|
embed.withDescription("❌ Неверно! Правильный ответ: **" + result.count() + "**\n\n```\n"
|
||||||
|
+ String.join("\n", result.grid()) + "\n```");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(embed.build()).queue();
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Неверный аргумент. Используй `!fish easy/normal/hard` или `!fish <число>`")
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
package creator.loki.feature.economy.command.minigame;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.feature.economy.service.JobService;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
public class JobCommand implements Command {
|
||||||
|
|
||||||
|
private final JobService jobService;
|
||||||
|
|
||||||
|
public JobCommand(JobService jobService) {
|
||||||
|
this.jobService = jobService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "job";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Управление работой — список, присоединиться";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "job — текущая работа\njob list — список работ\njob join <название> — устроиться";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Экономика";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
var args = context.getArgs();
|
||||||
|
var userId = context.getAuthor().getId();
|
||||||
|
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
handleCurrent(context, userId);
|
||||||
|
} else if (args.get(0).equalsIgnoreCase("list")) {
|
||||||
|
handleList(context);
|
||||||
|
} else if (args.get(0).equalsIgnoreCase("join")) {
|
||||||
|
if (args.size() < 2) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Укажи название работы: `!job join <название>`")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleJoin(context, userId, args.get(1));
|
||||||
|
} else {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.warning("Работа", "Используй: `!job`, `!job list`, `!job join <название>`")
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCurrent(CommandContext context, String userId) {
|
||||||
|
String current = jobService.getCurrentJob(userId);
|
||||||
|
if (current == null) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.info("Работа", "У тебя нет работы. Используй `!job list` чтобы посмотреть доступные.")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JobService.JobInfo info = findJob(current);
|
||||||
|
if (info == null) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.warning("Работа", "Текущая работа: **" + current + "**")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("💼 Твоя работа")
|
||||||
|
.withColor(new Color(100, 200, 255))
|
||||||
|
.addField("Должность", info.emoji() + " " + info.displayName(), false)
|
||||||
|
.addField("Зарплата", info.minPayout() + " – " + info.maxPayout() + " LC", true)
|
||||||
|
.addField("Требуемый уровень", String.valueOf(info.requiredLevel()), true)
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleList(CommandContext context) {
|
||||||
|
var jobs = jobService.listJobs();
|
||||||
|
var desc = new StringBuilder();
|
||||||
|
|
||||||
|
for (var job : jobs) {
|
||||||
|
desc.append(job.emoji()).append(" **").append(job.displayName())
|
||||||
|
.append("** — ").append(job.minPayout()).append("–").append(job.maxPayout())
|
||||||
|
.append(" LC (ур. ").append(job.requiredLevel()).append(")\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("📋 Доступные работы")
|
||||||
|
.withDescription(desc.toString())
|
||||||
|
.withColor(new Color(100, 200, 255))
|
||||||
|
.withFooter("!job join <название> — устроиться на работу")
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleJoin(CommandContext context, String userId, String name) {
|
||||||
|
var jobs = jobService.listJobs();
|
||||||
|
JobService.JobInfo target = null;
|
||||||
|
|
||||||
|
for (var job : jobs) {
|
||||||
|
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
|
||||||
|
target = job;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Работа **" + name + "** не найдена. Используй `!job list`")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jobService.setJob(userId, target.name());
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.success("Устройство на работу",
|
||||||
|
"Ты устроился на работу **" + target.emoji() + " " + target.displayName() + "**!\n"
|
||||||
|
+ "Используй `!work` чтобы начать работать.")
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JobService.JobInfo findJob(String name) {
|
||||||
|
for (var job : jobService.listJobs()) {
|
||||||
|
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
package creator.loki.feature.economy.command.minigame;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.feature.economy.service.EconomyService;
|
||||||
|
import creator.loki.feature.economy.service.SlotService;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
public class SlotCommand implements Command {
|
||||||
|
|
||||||
|
private final SlotService slotService;
|
||||||
|
private final EconomyService economyService;
|
||||||
|
|
||||||
|
public SlotCommand(SlotService slotService, EconomyService economyService) {
|
||||||
|
this.slotService = slotService;
|
||||||
|
this.economyService = economyService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "slot";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Крути слот-машину и выигрывай";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "slot <ставка>";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Экономика";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
var args = context.getArgs();
|
||||||
|
var userId = context.getAuthor().getId();
|
||||||
|
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.warning("Слоты", "Используй: `!slot <ставка>`")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long bet;
|
||||||
|
try {
|
||||||
|
bet = Long.parseLong(args.get(0));
|
||||||
|
if (bet <= 0) throw new NumberFormatException();
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Ставка должна быть положительным числом!")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var account = economyService.getAccount(userId);
|
||||||
|
if (account.getCash() < bet) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Недостаточно средств! Твой баланс: " + account.getCash() + " LC")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SlotService.SlotResult result = slotService.spin(userId, bet);
|
||||||
|
|
||||||
|
var comboDisplay = result.emoji1() + " | " + result.emoji2() + " | " + result.emoji3();
|
||||||
|
var desc = new StringBuilder("**" + comboDisplay + "**\n");
|
||||||
|
|
||||||
|
if (result.multiplier() > 0) {
|
||||||
|
desc.append("\n🎰 **").append(result.comboName()).append("!**")
|
||||||
|
.append(" x").append(result.multiplier())
|
||||||
|
.append("\n\n+**").append(result.winAmount()).append("** LC");
|
||||||
|
} else {
|
||||||
|
desc.append("\n\n😔 Повезёт в следующий раз!");
|
||||||
|
}
|
||||||
|
|
||||||
|
desc.append("\n\nБаланс: **").append(result.cashAfter()).append("** LC");
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("🎰 Слот-машина")
|
||||||
|
.withDescription(desc.toString())
|
||||||
|
.withColor(new Color(255, 215, 0))
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package creator.loki.feature.economy.command.minigame;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.feature.economy.service.JobService;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
public class WorkCommand implements Command {
|
||||||
|
|
||||||
|
private final JobService jobService;
|
||||||
|
|
||||||
|
public WorkCommand(JobService jobService) {
|
||||||
|
this.jobService = jobService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "work";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Работать и зарабатывать деньги";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Экономика";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
var userId = context.getAuthor().getId();
|
||||||
|
|
||||||
|
String currentJob = jobService.getCurrentJob(userId);
|
||||||
|
if (currentJob == null) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.warning("Работа", "У тебя нет работы! Используй `!job list` чтобы выбрать.")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JobService.JobInfo info = findJob(currentJob);
|
||||||
|
JobService.WorkResult result = jobService.work(userId, 0);
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "⏳ Ты уже работал сегодня! Отдохни и возвращайся завтра.")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String emoji = info != null ? info.emoji() : "💼";
|
||||||
|
String jobName = info != null ? info.displayName() : currentJob;
|
||||||
|
|
||||||
|
context.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("💼 Работа завершена")
|
||||||
|
.withColor(new Color(100, 200, 255))
|
||||||
|
.addField("Должность", emoji + " " + jobName, false)
|
||||||
|
.addField("Заработано", "+**" + result.amount() + "** LC", true)
|
||||||
|
.addField("Новый баланс", "**" + result.newCash() + "** LC", true)
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JobService.JobInfo findJob(String name) {
|
||||||
|
for (var job : jobService.listJobs()) {
|
||||||
|
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
package creator.loki.feature.economy.command.social;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.core.security.CooldownManager;
|
||||||
|
import creator.loki.feature.economy.service.EconomyService;
|
||||||
|
import creator.loki.embed.EmbedFactory;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public class RobCommand implements Command {
|
||||||
|
private final EconomyService economyService;
|
||||||
|
private final CooldownManager cooldownManager;
|
||||||
|
private final Random random;
|
||||||
|
|
||||||
|
public RobCommand(EconomyService economyService, CooldownManager cooldownManager) {
|
||||||
|
this.economyService = economyService;
|
||||||
|
this.cooldownManager = cooldownManager;
|
||||||
|
this.random = ThreadLocalRandom.current();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "rob"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() { return "Ограбить пользователя"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() { return "rob <@user>"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() { return "Экономика"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext ctx) {
|
||||||
|
List<Member> mentioned = ctx.getMessage().getMentions().getMembers();
|
||||||
|
if (mentioned.isEmpty()) {
|
||||||
|
ctx.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Укажите пользователя: " + getUsage())
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Member target = mentioned.get(0);
|
||||||
|
String uid = ctx.getAuthor().getId();
|
||||||
|
String tid = target.getId();
|
||||||
|
if (tid.equals(uid)) {
|
||||||
|
ctx.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "Нельзя ограбить самого себя!")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cooldownManager.isOnCooldown(uid, "rob")) {
|
||||||
|
long rem = cooldownManager.getRemainingSeconds(uid, "rob");
|
||||||
|
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.warning("Кдн",
|
||||||
|
"Подожди ещё **" + (rem / 3600) + "ч " + ((rem % 3600) / 60) + "м**")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cooldownManager.applyCooldown(uid, "rob", 86400);
|
||||||
|
var sender = economyService.getAccount(uid);
|
||||||
|
var victim = economyService.getAccount(tid);
|
||||||
|
if (sender.getCash() <= 0 || victim.getCash() <= 0) {
|
||||||
|
ctx.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedTemplates.error("Ошибка", "У одного из вас нет наличных!")
|
||||||
|
).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (random.nextDouble() < 0.4) {
|
||||||
|
long steal = Math.min(Math.min((long) (victim.getCash() * 0.15), 10000L), victim.getCash());
|
||||||
|
if (steal <= 0) {
|
||||||
|
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.info("Ограбление", "Слишком мелкая цель...")).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
economyService.transfer(tid, uid, steal);
|
||||||
|
ctx.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("🔫 Ограбление удалось!")
|
||||||
|
.withDescription("Ты украл **" + steal + "** LC у " + target.getAsMention())
|
||||||
|
.withColor(new Color(46, 204, 113))
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
} else {
|
||||||
|
long penalty = Math.min(Math.min((long) (sender.getCash() * 0.10), 5000L), sender.getCash());
|
||||||
|
if (penalty > 0) economyService.transfer(uid, tid, penalty);
|
||||||
|
ctx.getChannel().sendMessageEmbeds(
|
||||||
|
EmbedFactory.create()
|
||||||
|
.withTitle("👮 Ограбление провалено!")
|
||||||
|
.withDescription("Ты заплатил **" + penalty + "** LC " + target.getAsMention())
|
||||||
|
.withColor(new Color(231, 76, 60))
|
||||||
|
.build()
|
||||||
|
).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
package creator.loki.feature.economy.domain;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class EconomyAccount {
|
||||||
|
private final String userId;
|
||||||
|
private final long cash;
|
||||||
|
private final long bank;
|
||||||
|
private final long totalEarned;
|
||||||
|
private final long totalSpent;
|
||||||
|
private final String lastDaily;
|
||||||
|
private final int dailyStreak;
|
||||||
|
private final int fishingLuck;
|
||||||
|
private final int efficiency;
|
||||||
|
private final String lastFish;
|
||||||
|
private final String job;
|
||||||
|
private final String lastWork;
|
||||||
|
|
||||||
|
public EconomyAccount(String userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.cash = 0;
|
||||||
|
this.bank = 0;
|
||||||
|
this.totalEarned = 0;
|
||||||
|
this.totalSpent = 0;
|
||||||
|
this.lastDaily = null;
|
||||||
|
this.dailyStreak = 0;
|
||||||
|
this.fishingLuck = 0;
|
||||||
|
this.efficiency = 0;
|
||||||
|
this.lastFish = null;
|
||||||
|
this.job = null;
|
||||||
|
this.lastWork = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EconomyAccount(Builder builder) {
|
||||||
|
this.userId = builder.userId;
|
||||||
|
this.cash = builder.cash;
|
||||||
|
this.bank = builder.bank;
|
||||||
|
this.totalEarned = builder.totalEarned;
|
||||||
|
this.totalSpent = builder.totalSpent;
|
||||||
|
this.lastDaily = builder.lastDaily;
|
||||||
|
this.dailyStreak = builder.dailyStreak;
|
||||||
|
this.fishingLuck = builder.fishingLuck;
|
||||||
|
this.efficiency = builder.efficiency;
|
||||||
|
this.lastFish = builder.lastFish;
|
||||||
|
this.job = builder.job;
|
||||||
|
this.lastWork = builder.lastWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserId() { return userId; }
|
||||||
|
public long getCash() { return cash; }
|
||||||
|
public long getBank() { return bank; }
|
||||||
|
public long getTotalEarned() { return totalEarned; }
|
||||||
|
public long getTotalSpent() { return totalSpent; }
|
||||||
|
public String getLastDaily() { return lastDaily; }
|
||||||
|
public int getDailyStreak() { return dailyStreak; }
|
||||||
|
public int getFishingLuck() { return fishingLuck; }
|
||||||
|
public int getEfficiency() { return efficiency; }
|
||||||
|
public String getLastFish() { return lastFish; }
|
||||||
|
public String getJob() { return job; }
|
||||||
|
public String getLastWork() { return lastWork; }
|
||||||
|
|
||||||
|
public long getTotalBalance() { return cash + bank; }
|
||||||
|
|
||||||
|
public EconomyAccount withCash(long cash) {
|
||||||
|
return new Builder(this).cash(cash).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EconomyAccount withBank(long bank) {
|
||||||
|
return new Builder(this).bank(bank).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EconomyAccount withAddedEarned(long amount) {
|
||||||
|
return new Builder(this).totalEarned(this.totalEarned + amount).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EconomyAccount withAddedSpent(long amount) {
|
||||||
|
return new Builder(this).totalSpent(this.totalSpent + amount).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EconomyAccount withFishingLuck(int level) {
|
||||||
|
return new Builder(this).fishingLuck(level).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EconomyAccount withEfficiency(int level) {
|
||||||
|
return new Builder(this).efficiency(level).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
EconomyAccount that = (EconomyAccount) o;
|
||||||
|
return Objects.equals(userId, that.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() { return Objects.hash(userId); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("EconomyAccount[%s: Cash %d, Bank %d]",
|
||||||
|
userId, cash, bank);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private String userId;
|
||||||
|
private long cash;
|
||||||
|
private long bank;
|
||||||
|
private long totalEarned;
|
||||||
|
private long totalSpent;
|
||||||
|
private String lastDaily;
|
||||||
|
private int dailyStreak;
|
||||||
|
private int fishingLuck;
|
||||||
|
private int efficiency;
|
||||||
|
private String lastFish;
|
||||||
|
private String job;
|
||||||
|
private String lastWork;
|
||||||
|
|
||||||
|
public Builder() {}
|
||||||
|
|
||||||
|
public Builder(EconomyAccount existing) {
|
||||||
|
this.userId = existing.userId;
|
||||||
|
this.cash = existing.cash;
|
||||||
|
this.bank = existing.bank;
|
||||||
|
this.totalEarned = existing.totalEarned;
|
||||||
|
this.totalSpent = existing.totalSpent;
|
||||||
|
this.lastDaily = existing.lastDaily;
|
||||||
|
this.dailyStreak = existing.dailyStreak;
|
||||||
|
this.fishingLuck = existing.fishingLuck;
|
||||||
|
this.efficiency = existing.efficiency;
|
||||||
|
this.lastFish = existing.lastFish;
|
||||||
|
this.job = existing.job;
|
||||||
|
this.lastWork = existing.lastWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder userId(String userId) { this.userId = userId; return this; }
|
||||||
|
public Builder cash(long cash) { this.cash = cash; return this; }
|
||||||
|
public Builder bank(long bank) { this.bank = bank; return this; }
|
||||||
|
public Builder totalEarned(long totalEarned) { this.totalEarned = totalEarned; return this; }
|
||||||
|
public Builder totalSpent(long totalSpent) { this.totalSpent = totalSpent; return this; }
|
||||||
|
public Builder lastDaily(String lastDaily) { this.lastDaily = lastDaily; return this; }
|
||||||
|
public Builder dailyStreak(int dailyStreak) { this.dailyStreak = dailyStreak; return this; }
|
||||||
|
public Builder fishingLuck(int fishingLuck) { this.fishingLuck = fishingLuck; return this; }
|
||||||
|
public Builder efficiency(int efficiency) { this.efficiency = efficiency; return this; }
|
||||||
|
public Builder lastFish(String lastFish) { this.lastFish = lastFish; return this; }
|
||||||
|
public Builder job(String job) { this.job = job; return this; }
|
||||||
|
public Builder lastWork(String lastWork) { this.lastWork = lastWork; return this; }
|
||||||
|
|
||||||
|
public EconomyAccount build() { return new EconomyAccount(this); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package creator.loki.feature.economy.domain;
|
||||||
|
|
||||||
|
public record EconomyTransaction(
|
||||||
|
String fromId,
|
||||||
|
String toId,
|
||||||
|
long amount,
|
||||||
|
String type,
|
||||||
|
long timestamp
|
||||||
|
) {}
|
||||||
39
src/main/java/creator/loki/feature/economy/domain/Job.java
Normal file
39
src/main/java/creator/loki/feature/economy/domain/Job.java
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
package creator.loki.feature.economy.domain;
|
||||||
|
|
||||||
|
public enum Job {
|
||||||
|
WOODCUTTER("Лесоруб", 0, 15, 30, "\uD83E\uDE93"),
|
||||||
|
MINER("Шахтёр", 0, 20, 40, "\u26CF\uFE0F"),
|
||||||
|
FISHERMAN("Рыбак", 0, 25, 50, "\uD83C\uDFA3"),
|
||||||
|
COOK("Повар", 15, 30, 60, "\uD83C\uDF73"),
|
||||||
|
PROGRAMMER("Программист", 20, 40, 80, "\uD83D\uDCBB"),
|
||||||
|
ASTRONAUT("Космонавт", 35, 50, 100, "\uD83E\uDEB8");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
private final int requiredLevel;
|
||||||
|
private final int minPayout;
|
||||||
|
private final int maxPayout;
|
||||||
|
private final String emoji;
|
||||||
|
|
||||||
|
Job(String displayName, int requiredLevel, int minPayout, int maxPayout, String emoji) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.requiredLevel = requiredLevel;
|
||||||
|
this.minPayout = minPayout;
|
||||||
|
this.maxPayout = maxPayout;
|
||||||
|
this.emoji = emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() { return displayName; }
|
||||||
|
public int getRequiredLevel() { return requiredLevel; }
|
||||||
|
public int getMinPayout() { return minPayout; }
|
||||||
|
public int getMaxPayout() { return maxPayout; }
|
||||||
|
public String getEmoji() { return emoji; }
|
||||||
|
|
||||||
|
public static Job fromName(String name) {
|
||||||
|
for (Job j : values()) {
|
||||||
|
if (j.name().equalsIgnoreCase(name) || j.getDisplayName().equalsIgnoreCase(name)) {
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package creator.loki.feature.economy.domain;
|
||||||
|
|
||||||
|
public enum SlotCombination {
|
||||||
|
CHERRY(new String[]{"\uD83C\uDF52", "\uD83C\uDF52", "\uD83C\uDF52"}, 3, 10.0),
|
||||||
|
BELL(new String[]{"\uD83D\uDD14", "\uD83D\uDD14", "\uD83D\uDD14"}, 5, 5.0),
|
||||||
|
DIAMOND(new String[]{"\uD83D\uDC8E", "\uD83D\uDC8E", "\uD83D\uDC8E"}, 10, 2.0),
|
||||||
|
CROWN(new String[]{"\uD83D\uDC51", "\uD83D\uDC51", "\uD83D\uDC51"}, 15, 1.0),
|
||||||
|
JACKPOT(new String[]{"\u26A1", "\u26A1", "\u26A1"}, 50, 0.1);
|
||||||
|
|
||||||
|
private final String[] emojis;
|
||||||
|
private final int multiplier;
|
||||||
|
private final double chance;
|
||||||
|
|
||||||
|
SlotCombination(String[] emojis, int multiplier, double chance) {
|
||||||
|
this.emojis = emojis;
|
||||||
|
this.multiplier = multiplier;
|
||||||
|
this.chance = chance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getEmojis() { return emojis; }
|
||||||
|
public int getMultiplier() { return multiplier; }
|
||||||
|
public double getChance() { return chance; }
|
||||||
|
|
||||||
|
public static SlotCombination match(String e1, String e2, String e3) {
|
||||||
|
if (!e1.equals(e2) || !e2.equals(e3)) return null;
|
||||||
|
for (SlotCombination sc : values()) {
|
||||||
|
if (sc.emojis[0].equals(e1)) return sc;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package creator.loki.feature.economy.repository;
|
||||||
|
|
||||||
|
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface EconomyRepository {
|
||||||
|
Optional<EconomyAccount> findById(String userId);
|
||||||
|
void save(EconomyAccount account);
|
||||||
|
boolean exists(String userId);
|
||||||
|
List<EconomyAccount> findTopByBalance(int limit);
|
||||||
|
|
||||||
|
default EconomyAccount getAccount(String userId) {
|
||||||
|
return findById(userId).orElse(new EconomyAccount(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
package creator.loki.feature.economy.repository;
|
||||||
|
|
||||||
|
import creator.loki.database.Database;
|
||||||
|
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class SqlEconomyRepository implements EconomyRepository {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(SqlEconomyRepository.class);
|
||||||
|
private static final int MAX_TOP = 100;
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
public SqlEconomyRepository(Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<EconomyAccount> findById(String userId) {
|
||||||
|
String sql = """
|
||||||
|
SELECT user_id, cash, bank, total_earned, total_spent,
|
||||||
|
last_daily, daily_streak, fishing_luck, efficiency,
|
||||||
|
last_fish, job, last_work
|
||||||
|
FROM economy
|
||||||
|
WHERE user_id = ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, userId);
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return Optional.of(mapResultSetToAccount(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Error fetching economy account for {}", userId, e);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(EconomyAccount account) {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO economy (user_id, cash, bank, total_earned, total_spent,
|
||||||
|
last_daily, daily_streak, fishing_luck, efficiency,
|
||||||
|
last_fish, job, last_work)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(user_id)
|
||||||
|
DO UPDATE SET
|
||||||
|
cash = excluded.cash,
|
||||||
|
bank = excluded.bank,
|
||||||
|
total_earned = excluded.total_earned,
|
||||||
|
total_spent = excluded.total_spent,
|
||||||
|
last_daily = excluded.last_daily,
|
||||||
|
daily_streak = excluded.daily_streak,
|
||||||
|
fishing_luck = excluded.fishing_luck,
|
||||||
|
efficiency = excluded.efficiency,
|
||||||
|
last_fish = excluded.last_fish,
|
||||||
|
job = excluded.job,
|
||||||
|
last_work = excluded.last_work
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, account.getUserId());
|
||||||
|
stmt.setLong(2, account.getCash());
|
||||||
|
stmt.setLong(3, account.getBank());
|
||||||
|
stmt.setLong(4, account.getTotalEarned());
|
||||||
|
stmt.setLong(5, account.getTotalSpent());
|
||||||
|
if (account.getLastDaily() != null) {
|
||||||
|
stmt.setString(6, account.getLastDaily());
|
||||||
|
} else {
|
||||||
|
stmt.setNull(6, java.sql.Types.VARCHAR);
|
||||||
|
}
|
||||||
|
stmt.setInt(7, account.getDailyStreak());
|
||||||
|
stmt.setInt(8, account.getFishingLuck());
|
||||||
|
stmt.setInt(9, account.getEfficiency());
|
||||||
|
if (account.getLastFish() != null) {
|
||||||
|
stmt.setString(10, account.getLastFish());
|
||||||
|
} else {
|
||||||
|
stmt.setNull(10, java.sql.Types.VARCHAR);
|
||||||
|
}
|
||||||
|
if (account.getJob() != null) {
|
||||||
|
stmt.setString(11, account.getJob());
|
||||||
|
} else {
|
||||||
|
stmt.setNull(11, java.sql.Types.VARCHAR);
|
||||||
|
}
|
||||||
|
if (account.getLastWork() != null) {
|
||||||
|
stmt.setString(12, account.getLastWork());
|
||||||
|
} else {
|
||||||
|
stmt.setNull(12, java.sql.Types.VARCHAR);
|
||||||
|
}
|
||||||
|
stmt.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Error saving economy account for {}", account.getUserId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean exists(String userId) {
|
||||||
|
String sql = "SELECT 1 FROM economy WHERE user_id = ?";
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, userId);
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
return rs.next();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Error checking economy existence for {}", userId, e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<EconomyAccount> findTopByBalance(int limit) {
|
||||||
|
int lim = clampLimit(limit);
|
||||||
|
String sql = """
|
||||||
|
SELECT user_id, cash, bank, total_earned, total_spent,
|
||||||
|
last_daily, daily_streak, fishing_luck, efficiency,
|
||||||
|
last_fish, job, last_work
|
||||||
|
FROM economy
|
||||||
|
ORDER BY (cash + bank) DESC, cash DESC, user_id ASC
|
||||||
|
LIMIT ?
|
||||||
|
""";
|
||||||
|
List<EconomyAccount> list = new ArrayList<>();
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setInt(1, lim);
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
list.add(mapResultSetToAccount(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Error fetching top by balance", e);
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clampLimit(int limit) {
|
||||||
|
if (limit <= 0) return 1;
|
||||||
|
return Math.min(limit, MAX_TOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EconomyAccount mapResultSetToAccount(ResultSet rs) throws SQLException {
|
||||||
|
return new EconomyAccount.Builder()
|
||||||
|
.userId(rs.getString("user_id"))
|
||||||
|
.cash(rs.getLong("cash"))
|
||||||
|
.bank(rs.getLong("bank"))
|
||||||
|
.totalEarned(rs.getLong("total_earned"))
|
||||||
|
.totalSpent(rs.getLong("total_spent"))
|
||||||
|
.lastDaily(rs.getString("last_daily"))
|
||||||
|
.dailyStreak(rs.getInt("daily_streak"))
|
||||||
|
.fishingLuck(rs.getInt("fishing_luck"))
|
||||||
|
.efficiency(rs.getInt("efficiency"))
|
||||||
|
.lastFish(rs.getString("last_fish"))
|
||||||
|
.job(rs.getString("job"))
|
||||||
|
.lastWork(rs.getString("last_work"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
package creator.loki.feature.economy.service;
|
||||||
|
|
||||||
|
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||||
|
import creator.loki.feature.economy.repository.EconomyRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public class BlackjackService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(BlackjackService.class);
|
||||||
|
|
||||||
|
private static final String[] DECK = {"2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
|
||||||
|
|
||||||
|
private final EconomyRepository repository;
|
||||||
|
private final ConcurrentHashMap<String, ActiveBlackjack> activeGames = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public BlackjackService(EconomyRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ActiveBlackjack(List<String> playerCards, List<String> dealerCards, long bet, String userId) {}
|
||||||
|
|
||||||
|
public BlackjackState startGame(String userId, long bet) {
|
||||||
|
if (bet <= 0 || activeGames.containsKey(userId)) return null;
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
if (account.getCash() < bet) return null;
|
||||||
|
|
||||||
|
repository.save(account.withCash(account.getCash() - bet));
|
||||||
|
|
||||||
|
List<String> playerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
|
||||||
|
List<String> dealerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
|
||||||
|
|
||||||
|
int playerSum = calculateSum(playerCards);
|
||||||
|
int dealerSum = calculateSum(dealerCards);
|
||||||
|
boolean playerBj = playerSum == 21;
|
||||||
|
boolean dealerBj = dealerSum == 21;
|
||||||
|
|
||||||
|
if (playerBj || dealerBj) {
|
||||||
|
long payout;
|
||||||
|
if (playerBj && !dealerBj) {
|
||||||
|
payout = (long) (bet * 2.5);
|
||||||
|
} else {
|
||||||
|
payout = 0;
|
||||||
|
}
|
||||||
|
repository.save(repository.getAccount(userId).withCash(repository.getAccount(userId).getCash() + payout));
|
||||||
|
return new BlackjackState(
|
||||||
|
playerCards.toArray(String[]::new), dealerCards.toArray(String[]::new),
|
||||||
|
playerSum, dealerSum, bet, true, payout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveBlackjack game = new ActiveBlackjack(playerCards, dealerCards, bet, userId);
|
||||||
|
activeGames.put(userId, game);
|
||||||
|
return new BlackjackState(
|
||||||
|
playerCards.toArray(String[]::new), dealerCards.toArray(String[]::new),
|
||||||
|
playerSum, dealerSum, bet, false, 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlackjackState hit(String userId) {
|
||||||
|
ActiveBlackjack game = activeGames.get(userId);
|
||||||
|
if (game == null) return null;
|
||||||
|
|
||||||
|
game.playerCards().add(drawCard());
|
||||||
|
int sum = calculateSum(game.playerCards());
|
||||||
|
|
||||||
|
if (sum > 21) {
|
||||||
|
activeGames.remove(userId);
|
||||||
|
return new BlackjackState(
|
||||||
|
game.playerCards().toArray(String[]::new), game.dealerCards().toArray(String[]::new),
|
||||||
|
sum, calculateSum(game.dealerCards()), game.bet(), true, 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BlackjackState(
|
||||||
|
game.playerCards().toArray(String[]::new), game.dealerCards().toArray(String[]::new),
|
||||||
|
sum, calculateSum(game.dealerCards()), game.bet(), false, 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlackjackState stand(String userId) {
|
||||||
|
ActiveBlackjack game = activeGames.remove(userId);
|
||||||
|
if (game == null) return null;
|
||||||
|
|
||||||
|
List<String> dealerCards = new ArrayList<>(game.dealerCards());
|
||||||
|
while (calculateSum(dealerCards) < 16) {
|
||||||
|
dealerCards.add(drawCard());
|
||||||
|
}
|
||||||
|
|
||||||
|
int playerSum = calculateSum(game.playerCards());
|
||||||
|
int dealerSum = calculateSum(dealerCards);
|
||||||
|
long payout;
|
||||||
|
|
||||||
|
if (dealerSum > 21 || playerSum > dealerSum) {
|
||||||
|
payout = game.bet() * 2;
|
||||||
|
} else {
|
||||||
|
payout = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
repository.save(account.withCash(account.getCash() + payout));
|
||||||
|
|
||||||
|
return new BlackjackState(
|
||||||
|
game.playerCards().toArray(String[]::new), dealerCards.toArray(String[]::new),
|
||||||
|
playerSum, dealerSum, game.bet(), true, payout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String drawCard() {
|
||||||
|
return DECK[ThreadLocalRandom.current().nextInt(DECK.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private int calculateSum(List<String> cards) {
|
||||||
|
int sum = 0;
|
||||||
|
int aces = 0;
|
||||||
|
for (String c : cards) {
|
||||||
|
switch (c) {
|
||||||
|
case "J", "Q", "K" -> sum += 10;
|
||||||
|
case "A" -> { sum += 11; aces++; }
|
||||||
|
default -> sum += Integer.parseInt(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (sum > 21 && aces > 0) {
|
||||||
|
sum -= 10;
|
||||||
|
aces--;
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BlackjackState(String[] playerCards, String[] dealerCards, int playerSum, int dealerSum, long bet, boolean isOver, long payout) {}
|
||||||
|
|
||||||
|
public record BlackjackResult(boolean playerWon, long payout, String reason) {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
package creator.loki.feature.economy.service;
|
||||||
|
|
||||||
|
import creator.loki.core.security.CooldownManager;
|
||||||
|
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||||
|
import creator.loki.feature.economy.repository.EconomyRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class EconomyService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(EconomyService.class);
|
||||||
|
|
||||||
|
private final EconomyRepository repository;
|
||||||
|
private final CooldownManager cooldownManager;
|
||||||
|
|
||||||
|
public EconomyService(EconomyRepository repository, CooldownManager cooldownManager) {
|
||||||
|
this.repository = repository;
|
||||||
|
this.cooldownManager = cooldownManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EconomyAccount getAccount(String userId) {
|
||||||
|
return repository.getAccount(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean transfer(String fromId, String toId, long amount) {
|
||||||
|
if (amount <= 0) return false;
|
||||||
|
EconomyAccount from = repository.getAccount(fromId);
|
||||||
|
if (from.getCash() < amount) return false;
|
||||||
|
EconomyAccount to = repository.getAccount(toId);
|
||||||
|
repository.save(from.withCash(from.getCash() - amount));
|
||||||
|
repository.save(to.withCash(to.getCash() + amount));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deposit(String userId, long amount) {
|
||||||
|
if (amount <= 0) return false;
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
if (account.getCash() < amount) return false;
|
||||||
|
repository.save(account.withCash(account.getCash() - amount).withBank(account.getBank() + amount));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean withdraw(String userId, long amount) {
|
||||||
|
if (amount <= 0) return false;
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
if (account.getBank() < amount) return false;
|
||||||
|
repository.save(account.withBank(account.getBank() - amount).withCash(account.getCash() + amount));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long earnCash(String userId, long amount) {
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
EconomyAccount updated = account.withCash(account.getCash() + amount).withAddedEarned(amount);
|
||||||
|
repository.save(updated);
|
||||||
|
return updated.getCash();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean spendCash(String userId, long amount) {
|
||||||
|
if (amount <= 0) return false;
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
if (account.getCash() < amount) return false;
|
||||||
|
repository.save(account.withCash(account.getCash() - amount).withAddedSpent(amount));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DailyResult claimDaily(String userId) {
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
String today = LocalDate.now().toString();
|
||||||
|
if (today.equals(account.getLastDaily())) return null;
|
||||||
|
String yesterday = LocalDate.now().minusDays(1).toString();
|
||||||
|
int streak;
|
||||||
|
if (yesterday.equals(account.getLastDaily())) {
|
||||||
|
streak = account.getDailyStreak() + 1;
|
||||||
|
} else {
|
||||||
|
streak = 1;
|
||||||
|
}
|
||||||
|
long amount = streak >= 7 ? (long) (50 * 1.5) : 50;
|
||||||
|
long newCash = account.getCash() + amount;
|
||||||
|
EconomyAccount updated = new EconomyAccount.Builder(account)
|
||||||
|
.cash(newCash)
|
||||||
|
.totalEarned(account.getTotalEarned() + amount)
|
||||||
|
.lastDaily(today)
|
||||||
|
.dailyStreak(streak)
|
||||||
|
.build();
|
||||||
|
repository.save(updated);
|
||||||
|
return new DailyResult(amount, streak, newCash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int upgradeFishingLuck(String userId) {
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
if (account.getFishingLuck() >= 5) return -1;
|
||||||
|
long cost = 800L * (account.getFishingLuck() + 1);
|
||||||
|
if (account.getCash() < cost) return -1;
|
||||||
|
int newLevel = account.getFishingLuck() + 1;
|
||||||
|
repository.save(account.withCash(account.getCash() - cost).withFishingLuck(newLevel).withAddedSpent(cost));
|
||||||
|
return newLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int upgradeEfficiency(String userId) {
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
if (account.getEfficiency() >= 5) return -1;
|
||||||
|
long cost = 800L * (account.getEfficiency() + 1);
|
||||||
|
if (account.getCash() < cost) return -1;
|
||||||
|
int newLevel = account.getEfficiency() + 1;
|
||||||
|
repository.save(account.withCash(account.getCash() - cost).withEfficiency(newLevel).withAddedSpent(cost));
|
||||||
|
return newLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EconomyAccount> getTopBalances(int limit) {
|
||||||
|
return repository.findTopByBalance(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DailyResult(long amount, int streak, long newCash) {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
package creator.loki.feature.economy.service;
|
||||||
|
|
||||||
|
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||||
|
import creator.loki.feature.economy.repository.EconomyRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public class FishingService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(FishingService.class);
|
||||||
|
|
||||||
|
private static final String[] FISH = {"\uD83D\uDC1F", "\uD83D\uDC20", "\uD83D\uDC21", "\uD83D\uDC19"};
|
||||||
|
private static final String WAVE = "\uD83C\uDF0A";
|
||||||
|
private static final long COOLDOWN_MS = 7200000;
|
||||||
|
|
||||||
|
private final EconomyRepository repository;
|
||||||
|
private final ConcurrentHashMap<String, ActiveFish> activeFishing = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public FishingService(EconomyRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ActiveFish(String targetFish, int correctCount, long startTime, String difficulty, String[] grid) {}
|
||||||
|
|
||||||
|
public FishResult startFishing(String userId, String difficulty) {
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
if (account.getLastFish() != null) {
|
||||||
|
long elapsed = System.currentTimeMillis() - Long.parseLong(account.getLastFish());
|
||||||
|
if (elapsed < COOLDOWN_MS) return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size;
|
||||||
|
int fishTypes;
|
||||||
|
int baseReward;
|
||||||
|
long timeLimit;
|
||||||
|
switch (difficulty.toLowerCase()) {
|
||||||
|
case "easy" -> { size = 5; fishTypes = 2; baseReward = 15; timeLimit = 30000; }
|
||||||
|
case "normal" -> { size = 7; fishTypes = 3; baseReward = 30; timeLimit = 45000; }
|
||||||
|
case "hard" -> { size = 9; fishTypes = 4; baseReward = 50; timeLimit = 60000; }
|
||||||
|
default -> { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] available = Arrays.copyOf(FISH, fishTypes);
|
||||||
|
String[][] grid = new String[size][size];
|
||||||
|
int[] counts = new int[fishTypes];
|
||||||
|
var rng = ThreadLocalRandom.current();
|
||||||
|
|
||||||
|
for (int r = 0; r < size; r++) {
|
||||||
|
for (int c = 0; c < size; c++) {
|
||||||
|
if (rng.nextInt(100) < 40) {
|
||||||
|
grid[r][c] = WAVE;
|
||||||
|
} else {
|
||||||
|
int fi = rng.nextInt(fishTypes);
|
||||||
|
grid[r][c] = available[fi];
|
||||||
|
counts[fi]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int targetIdx;
|
||||||
|
do {
|
||||||
|
targetIdx = rng.nextInt(fishTypes);
|
||||||
|
} while (counts[targetIdx] == 0);
|
||||||
|
|
||||||
|
String targetFish = available[targetIdx];
|
||||||
|
int correctCount = counts[targetIdx];
|
||||||
|
|
||||||
|
String[] rows = new String[size];
|
||||||
|
for (int r = 0; r < size; r++) {
|
||||||
|
rows[r] = String.join(" ", grid[r]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveFish session = new ActiveFish(targetFish, correctCount, System.currentTimeMillis(), difficulty, rows);
|
||||||
|
activeFishing.put(userId, session);
|
||||||
|
|
||||||
|
return new FishResult(rows, targetFish, correctCount, 0, account.getCash());
|
||||||
|
}
|
||||||
|
|
||||||
|
public FishResult checkAnswer(String userId, int answer, long startTime, String difficulty) {
|
||||||
|
ActiveFish session = activeFishing.remove(userId);
|
||||||
|
if (session == null) return null;
|
||||||
|
if (session.startTime() != startTime || !session.difficulty().equals(difficulty)) return null;
|
||||||
|
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
long elapsed = System.currentTimeMillis() - session.startTime();
|
||||||
|
|
||||||
|
int baseReward;
|
||||||
|
long timeLimit;
|
||||||
|
switch (difficulty.toLowerCase()) {
|
||||||
|
case "easy" -> { baseReward = 15; timeLimit = 30000; }
|
||||||
|
case "normal" -> { baseReward = 30; timeLimit = 45000; }
|
||||||
|
case "hard" -> { baseReward = 50; timeLimit = 60000; }
|
||||||
|
default -> { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
long reward = 0;
|
||||||
|
if (answer == session.correctCount() && elapsed <= timeLimit) {
|
||||||
|
double ratio = (double) elapsed / timeLimit;
|
||||||
|
double speedMult = ratio < 0.3 ? 2.0 : ratio < 0.5 ? 1.5 : 1.0;
|
||||||
|
reward = (long) (baseReward * speedMult * (1 + account.getFishingLuck() * 0.01));
|
||||||
|
}
|
||||||
|
|
||||||
|
long newCash = account.getCash() + reward;
|
||||||
|
EconomyAccount updated = new EconomyAccount.Builder(account)
|
||||||
|
.cash(newCash)
|
||||||
|
.totalEarned(account.getTotalEarned() + reward)
|
||||||
|
.lastFish(String.valueOf(System.currentTimeMillis()))
|
||||||
|
.build();
|
||||||
|
repository.save(updated);
|
||||||
|
|
||||||
|
return new FishResult(session.grid(), session.targetFish(), session.correctCount(), reward, newCash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record FishResult(String[] grid, String targetFish, int count, long reward, long cashAfter) {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package creator.loki.feature.economy.service;
|
||||||
|
|
||||||
|
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||||
|
import creator.loki.feature.economy.domain.Job;
|
||||||
|
import creator.loki.feature.economy.repository.EconomyRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public class JobService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(JobService.class);
|
||||||
|
|
||||||
|
private static final long WORK_COOLDOWN = 21600000;
|
||||||
|
|
||||||
|
private final EconomyRepository repository;
|
||||||
|
|
||||||
|
public JobService(EconomyRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJob(String userId, String jobName) {
|
||||||
|
Job job = Job.fromName(jobName);
|
||||||
|
if (job == null) return;
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
EconomyAccount updated = new EconomyAccount.Builder(account)
|
||||||
|
.job(job.name())
|
||||||
|
.build();
|
||||||
|
repository.save(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorkResult work(String userId, int userLevel) {
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
String jobName = account.getJob();
|
||||||
|
if (jobName == null) return null;
|
||||||
|
|
||||||
|
Job job = Job.fromName(jobName);
|
||||||
|
if (job == null) return null;
|
||||||
|
if (userLevel < job.getRequiredLevel()) return null;
|
||||||
|
|
||||||
|
if (account.getLastWork() != null) {
|
||||||
|
long elapsed = System.currentTimeMillis() - Long.parseLong(account.getLastWork());
|
||||||
|
if (elapsed < WORK_COOLDOWN) return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rng = ThreadLocalRandom.current();
|
||||||
|
long basePayout = rng.nextLong(job.getMinPayout(), job.getMaxPayout() + 1);
|
||||||
|
long amount = (long) (basePayout * (1 + account.getEfficiency() * 0.01));
|
||||||
|
|
||||||
|
long newCash = account.getCash() + amount;
|
||||||
|
EconomyAccount updated = new EconomyAccount.Builder(account)
|
||||||
|
.cash(newCash)
|
||||||
|
.totalEarned(account.getTotalEarned() + amount)
|
||||||
|
.lastWork(String.valueOf(System.currentTimeMillis()))
|
||||||
|
.build();
|
||||||
|
repository.save(updated);
|
||||||
|
|
||||||
|
return new WorkResult(amount, newCash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<JobInfo> listJobs() {
|
||||||
|
List<JobInfo> infos = new ArrayList<>();
|
||||||
|
for (Job job : Job.values()) {
|
||||||
|
infos.add(new JobInfo(
|
||||||
|
job.name(),
|
||||||
|
job.getDisplayName(),
|
||||||
|
job.getRequiredLevel(),
|
||||||
|
job.getMinPayout(),
|
||||||
|
job.getMaxPayout(),
|
||||||
|
job.getEmoji()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return infos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentJob(String userId) {
|
||||||
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
|
String jobName = account.getJob();
|
||||||
|
if (jobName == null) return null;
|
||||||
|
Job job = Job.fromName(jobName);
|
||||||
|
return job != null ? job.getDisplayName() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record WorkResult(long amount, long newCash) {}
|
||||||
|
|
||||||
|
public record JobInfo(String name, String displayName, int requiredLevel, int minPayout, int maxPayout, String emoji) {}
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue