discord-bot/TODO.md

629 lines
43 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# TODO — Loki Bot v2.1
## ✅ Что уже сделано
| # | Статус | Проблема | Изменения |
|---|--------|----------|-----------|
| C1–C15 | ✅ | Предыдущие раунды | См. git history |
| J21 | ✅ | Java 17 → 21 | `build.gradle`: source/target 21 |
| A05 | ✅ | EventDispatcher: CachedThreadPool → VirtualThreadPool | `Executors.newVirtualThreadPerTaskExecutor()` |
| A06 | ✅ | SwapCommand + RepTopCommand: `new Thread()` → `Thread.startVirtualThread()` | |
| A25 | ✅ | CommandExceptionHandler: instanceof pattern matching | |
| A26 | ✅ | ClearCommand: instanceof pattern matching | |
| A27 | ✅ | DatabaseMigration: instanceof pattern matching | |
| 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 |
| A11 | ✅ | RankCardGenerator: шрифты не кэшировались | ConcurrentHashMap cache |
| A12 | ✅ | RankCardGenerator: SVG-иконки не работали | Заменены на геометрические фигуры (Java2D) |
| A23 | ✅ | RankCardGenerator: throws Exception | loadFont безопасный |
| A03 | ✅ | Database: race condition на connection | synchronized методы |
| A13 | ✅ | NekosLifeApi: Thread.sleep в catch | Сон вынесен в retry loop |
| A14 | ✅ | GenerateRatingsCommand: блокировка event-потока | `complete()` вместо `queue()` |
| A35 | ✅ | GenerateRatingsCommand: опечатка | `Пропускаемботов` → `Пропускаем ботов` |
| A04 | ✅ | PrefixCommandRegistrar: God Constructor (22 param) | Разбит на 4 per-feature CommandRegistrar, Multibinder, 1 param |
| A21 | ✅ | BotModule: путь к БД хардкодом | Вынесен в .env (`DB_PATH`) + BotConfig |
| A34 | ✅ | CommandCategories: мёртвый код | Уже удалён (старый пакет) |
| A37 | ✅ | ProfileCommand: регистрация в PrefixCommandRegistrar | LevelCommandRegistrar |
| A41 | ✅ | TikTok: команда `!tiktok <url>` для скачивания видео | TikTokApi + TikTokCommand + TikTokModule |
| A42 | ✅ | S1-S5: SOLID рефакторинг | Lifecycle, JdaFactory, PlayLoadHandler, feature modules |
| A43 | ✅ | D4-D6: DRY рефакторинг | VoiceHelper.connectForPlayback, RatingConfig data-driven, SocialCommand.Flavor |
| A44 | ✅ | PlayCommand: 207→148 строк | PlayLoadHandler + VoiceHelper.connectForPlayback |
| A45 | ✅ | TrackScheduler: 146→113 строк | TrackStarter (retry логика) |
| A46 | ✅ | MuteCommand: 199→173 строки | execute() разбит на 6 методов |
| A47 | ✅ | BotInitializer: 162→135 строк | JdaFactory + LavalinkManagerFactory |
| A48 | ✅ | ShutdownManager: 9→3 зависимости | Lifecycle interface + Multibinder |
| A49 | ✅ | BotModule: 98→84 строки | CommandRegistrar bindings в feature модулях |
| SP1 | ✅ | Spotify не работал: lavasrc не мог получить токен (spotify-tokener упал с `-32000`, NPE 500 на loadtracks) | Удалён spotify-tokener, lavasrc → client credentials; плейбек Spotify → scsearch-мирроринг (SoundCloud) |
| PL1 | ✅ | Плейлист SoundCloud: игрался только 1 трек (REPLACED-гонка в TrackScheduler) | Локальный current-трекинг + атомарный skip + сохранение volume (`5e83356`) |
---
## ♻️ Правила разработки
- KISS, DRY, SOLID
- Файлы ≤150–200 строк (в идеале ≤100)
- В папке ≤6 файлов
- Пакет = домен/фича, а не тип файла (`feature/level/`, а не `commands/listeners/`)
- Каждая фича содержит свои команды, сервисы, репозитории
- Единый слой персистентности через `repository/`
- Интеграции в `integration/`
---
## 🔴 Проблемы (результаты аудита, которые ещё не исправлены)
### HIGH
| # | Приоритет | Проблема | Статус |
|---|-----------|----------|--------|
| A07 | 🟠 **HIGH** | `HealthHttpServer` — single thread executor | ✅ Virtual threads |
| 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()` |
### Аудит 30.08.2026 — баг-хант (3 суб-агента, не исправлено)
#### Экономика / уровень / модериование / config / voice / counter
| # | Приоритет | Проблема | Файл |
|---|-----------|----------|------|
| F1 | 🔴 **CRITICAL** | `!slot`: `cashAfter` пишется из устаревшего снимка аккаунта — параллельные спины = бесплатные спины / двойные списания. Нужен атомарный `UPDATE ... SET cash = cash - ? WHERE cash >= ?` | `SlotService.java:45-46` |
| F2 | 🔴 **CRITICAL** | `transfer()` неатомарный read-check-write без транзакции — двойная трата / потеря денег при параллельных переводах | `EconomyService.java:29-41` |
| F3 | 🟠 **HIGH** | `/setup` целиком без проверок прав — любой участник меняет счётчик, логи, voice-триггер, роли за уровни | `SetupSlashHandler.java:50-105`, `SetupInteractions.java:19-71` |
| F4 | 🟠 **HIGH** | `claimDaily()` check-then-act по `last_daily` — двойной дейли при параллельных запросах | `EconomyService.java:88-93` |
| F5 | 🟠 **HIGH** | `WorkCommand` передаёт `level=0` — работы с `requiredLevel > 0` (COOK/PROGRAMMER/ASTRONAUT) недоступны никогда | `WorkCommand.java:51` |
| F6 | 🟠 **HIGH** | `LevelResetService`: фильтр `guildId + ":"` не совпадает с ключами `user_stats` — сброс уровней никого не трогает, а одноразовый флаг сгорает | `LevelResetService.java:34-43` |
| F7 | 🟠 **HIGH** | Blackjack: `hit/stand` NPE при гонке (state==null после удаления), общий несинхронизированный ArrayList карт | `BlackjackCommand.java:125`, `BlackjackService.java:52-95` |
| F8 | 🟠 **HIGH** | Блокирующие JDBC / `.complete()` на gateway-потоке JDA: 4 SQL на каждое сообщение, SQL на join/leave, REST на создание канала | `MessageListener.java:42-48`, `MemberCounterListener.java:59-74`, `FishMessageListener.java:39`, `VoiceChannelListener.java:41`, `VoiceTracker.java:78-79` |
| F9 | 🟡 **MEDIUM** | Blackjack push (равные очки) — игрок теряет ставку, ничья не обработана | `BlackjackService.java:89` |
| F10 | 🟡 **MEDIUM** | Blackjack TOCTOU `containsKey` — двойное списание ставки при параллельных `!bj`; ставка теряется при рестарте (игры только в памяти) | `BlackjackService.java:22,48` |
| F11 | 🟡 **MEDIUM** | Рыбалка: кулдаун пишется только при верном ответе — можно бесконечно перегенерировать поле без штрафа | `FishingService.java:24-30,87` |
| F12 | 🟡 **MEDIUM** | Счётчик участников: `getMemberCount()` (с ботами) vs `countMembers()` (без) — число на канале прыгает | `CounterChannelUpdater.java:22`, `CounterManager.java:81` |
| F13 | 🟡 **MEDIUM** | XP/время в войсе: read-modify-write без синхронизации — терянные апдейты при параллельных начислениях | `LevelService.java:41-49` |
| F14 | 🟡 **MEDIUM** | `getSystemChannel()` @Nullable → NPE при level-up в войсе | `VoiceTracker.java:85` |
#### Music / radio
| # | Приоритет | Проблема | Файл |
|---|-----------|----------|------|
| M1 | 🟠 **HIGH** | Утечка потоков: `removeMusicManager()` не вызывает `destroy()` — каждый автолив оставляет навсегда живой `ScheduledExecutorService` | `LavalinkManager.java:96-98`, `AutoLeaveController.java:47` |
| M2 | 🟠 **HIGH** | `m!play`: блокирующий `connectForPlayback` (sleep до ~4с) прямо на gateway-потоке music-бота | `MusicBotPlayHandler.java:36` |
| M3 | 🟠 **HIGH** | Селект-меню поиска: то же — блокирующий `connectForPlayback` в `onStringSelectInteraction` | `PlaySelectHandler.java:55` |
| M4 | 🟡 **MEDIUM** | `asVoiceChannel()` кидает `IllegalStateException` в stage-каналах — команды падают у пользователей | `VoiceHelper.java:59`, `PlaySlashHandler.java:31`, `RadioBotCommands.java:40` |
| M5 | 🟡 **MEDIUM** | Гонка `RadioManager.startRadio` — двойной `!r!play` теряет GuildMusicManager (утечка), `connect()==null` оставляет «зомби-сессию» | `RadioManager.java:64-83` |
| M6 | 🟡 **MEDIUM** | Отложенный `loadItem` оживляет уничтоженное радио (enqueue на выключенном executor → `RejectedExecutionException` глотается) | `RadioStreamLoader.java:36` |
| M7 | 🟡 **MEDIUM** | `!queue`: embed-поле >1024 символов при длинных названиях → `IllegalArgumentException`, команда падает | `QueueView.java:29,51-64` |
| M8 | 🟢 **LOW** | `Integer.parseInt` переполняется на большом «номере трека» из поиска | `PlayCommand.java:83` |
| M9 | 🟢 **LOW** | Заголовок embed поиска не обрезан до 256 символов | `SearchHandler.java:50` |
| M10 | 🟢 **LOW** | TOCTOU `link.getCachedPlayer() != null ? ...getX()` — NPE между проверкой и вызовом | `QueueView.java:40`, `VolumeControl.java:11-22` |
| M11 | 🟢 **LOW** | `FilterState.customBands` — несинхронизированный ArrayList под конкурентным доступом | `FilterState.java:19,89` |
| M12 | 🟢 **LOW** | `m!play` игнорирует неудачу подключения — «Добавлено в очередь», а звука нет | `MusicBotPlayHandler.java:36` |
| M13 | 🟢 **LOW** | `destroy()` глотает `InterruptedException` без восстановления флага | `GuildMusicManager.java:64-66` |
| M14 | 🟢 **LOW** | `r!volume` рапортует успех при отсутствии радио-сессии | `RadioSubcommands.java:108-113` |
| M15 | 🟡 **MEDIUM** | Spotify-плейлисты не грузятся: Spotify (политика 27.11.2024) требует user-auth для `/v1/playlists/*` — client credentials получают `401 Valid user authentication required`. Одиночные треки работают (метаданные по client credentials + плейбек через scsearch-мирроринг). Варианты: OAuth refresh token юзера с scope `playlist-read-private`, либо revival browser-tokener | `lavalink/application.yml` (lavasrc) |
#### Core / database
| # | Приоритет | Проблема | Файл |
|---|-----------|----------|------|
| C1 | 🟠 **HIGH** | Прокси `PreparedStatement` не разворачивает `InvocationTargetException` → SQL-ошибки вылетают как `UndeclaredThrowableException`, все `catch (SQLException)` их не ловят | `Database.java:76-89` |
| C2 | 🟡 **MEDIUM** | Переполнение long в `MuteDurationParser` (`!mute 106751991167301d` → отрицательные секунды) | `MuteDurationParser.java:55-57` |
| C3 | 🟡 **MEDIUM** | `event.getGuild()` в ЛС кидает `IllegalStateException` (JDA 6.5.0) — префикс-команды в ЛС падают | `EventDispatcher.java:81,107` |
| C4 | 🟡 **MEDIUM** | `CooldownManager` check-then-act — два параллельных `!rep`/`!rob` обходят кулдаун | `CooldownManager.java:23-57` |
| C5 | 🟡 **MEDIUM** | Миграция продолжается после частичного провала, `migrated=true` ставится всегда — тихая потеря данных | `DatabaseMigration.java:40-44`, `JsonMigration.java:43,63,88,117` |
| C6 | 🟢 **LOW** | `PRAGMA foreign_keys = ON` выполняется на одном соединении, не на всех в пуле | `Database.java:43-48` |
| C7 | 🟢 **LOW** | `JsonMigration`: NPE на битых записях легаси-JSON → бот не стартует | `JsonMigration.java:33-39` |
| C8 | 🟢 **LOW** | Lavalink-клиенты не регистрируются в ShutdownManager — websocket не закрывается при graceful shutdown | `BotInitializer.java:73-83` |
| C9 | 🟢 **LOW** | Ответ «команда не найдена» до rate-limiter — флуд несуществующими командами не ограничен | `EventDispatcher.java:63-77` |
| C10 | 🟢 **LOW** | `ApiRequestQueue`: `future.get()` без таймаута; при прерывании возвращает `null` | `ApiRequestQueue.java:38` |
---
## 📋 План рефакторинга
### Этап 1: Java 21 ✅ (14 items)
- [x] EventDispatcher: VirtualThreadPool
- [x] instanceof pattern matching (3 файла)
- [x] arrow-switch (InfoCommand)
- [x] String.repeat() (RateCommand)
- [x] .toList() (3 файла)
- [x] Map.ofEntries() (HelpCategories)
### Этап 2: Рефакторинг PrefixCommandRegistrar ✅
- [x] Создан интерфейс `CommandRegistrar`
- [x] 4 per-feature регистратора
- [x] Guice Multibinder для сбора регистраторов
- [x] 22 параметра → 1 (`Set<CommandRegistrar>`)
### Этап 3: Critical-баги ✅
- [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/
---
## 🚀 Новые фичи (Roadmap)
## 🚀 Экономика — ✅ реализовано
### Модель данных
```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
);
```
### Заработок
| Механика | Доход | Кулдаун |
|---|---|---|
| Сообщение | 1–3 LC | 60с |
| Голос | 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 | — |
### Миниигры
**Рыбалка** (`!fish easy/normal/hard`)
- Сетка N×N (emoji + 🌊), бесплатно, неправильный ответ = 0
- Множитель скорости: <50% времени = ×1.5, <30% = ×2.0
- Стат `fishing_luck` (макс 5, стоимость 800×lvl)
**Блэкджек** (`!bj <bet>`)
- Дилер стоит на 16, ничья = проигрыш, BJ = ×1.5
- Кнопки Hit/Stand
**Слоты** (`!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
- [x] Система экономики (Economy) — **✅**
### Приоритет 2
- [x] Авто-модерация (3 варна = авто-мут на 1ч)
- [x] Музыка (lavaplayer: play, skip, stop, queue, np, volume, leave)
- [x] ~~Музыка — поиск с выбором (yt/sc/list + выбор номера)~~
- [x] Server Stats (участники, каналы, роли, бусты)
- [x] Level Roles (роли за уровни)
- [x] Красивые level-up embed с прогресс-баром
### Приоритет 3
- [x] Статистика сервера — графики, CSV export — ✅ `!serverstats [csv|chart]`
---
### 🐾 Животные — зоо-викторины и картинки (public-apis) — 🟢 в работе
- [x] `feature/animals/AnimalApi` — клиент к 8 API (thecatapi, dog.ceo, randomfox, shibe.online, some-random-api, nekos.life, waifu.pics, catboys.com) с fallback-цепочкой
- [x] `!catperson` / `!dogperson` / `!foxperson` — «Какой ты кот/пёс/лис»: % + роль + картинка (детерминировано ID)
- [x] `!shibe` / `!birb` — картинки шиба-ину и птичек
- [x] `!neko` / `!waifu` / `!catboy` — аниме-картинки (неко, вайфу, кошко-мальчик)
- [x] Факты: `!catfact` / `!dogfact` — факт + картинка (catfact.ninja, dog facts API)
- [x] `!joke` — анекдоты (jokeapi.dev, категории Any/Programming/Misc)
- [x] `!chuck` — цитаты Чака Норриса (api.chucknorris.io)
- [x] `!inspire` — мотивационные цитаты (zenquotes.io)
- [x] `!apod` — астрономическая картинка дня NASA (api.nasa.gov, DEMO_KEY)
- [x] `!iss` — где сейчас МКС (api.wheretheiss.at)
- [x] `!space` — кто сейчас в космосе (open-notify.org)
- [x] `!xkcd` — свежий стрип (xkcd.com/api)
- [x] `!meme <subreddit>` — расширить мем по сабреддиту
---
## ⚡ Стабильность и скорость (2026-08-19, аудит сервера 185.176.94.38)
### Найдено в логах
| # | Проблема | Статус |
|---|----------|--------|
| S01 | `LogEmbeds.ban`/`kick` — NPE `Value may not be null`, если бан/кик без причины (падал onSuccess-колбэк, терялся лог) | ✅ `reason != null ? reason : "Не указана"` |
| S02 | `RatingService.getDeterministicValue` — гонка на общем `Random.setSeed()`: 2 пользователя одновременно → результаты «подменялись» | ✅ локальный `new Random(seed)` (thread-safe) |
| S03 | Пул репортов на внешние API отсутствовал | ✅ `util/ApiRequestQueue` (пул 4, изоляция ответов) |
| S04 | `Missed 2 heartbeats` — 1 раз, транзиентный (сеть), не повторяется | ➖ наблюдать |
### Что сделано
- `util/ApiRequestQueue` — очередь исходящих запросов: каждый запрос выполняется изолированно, ответ не может подмешаться к соседнему, пул 4 не перегружает публичные API. Подключён в `AnimalApi` и `NekosLifeApi`, зарегистрирован в LifecycleModule (graceful shutdown)
- `RatingService` — детерминированное значение теперь без общего `Random` (root cause «подмены ответов»)
- `LogEmbeds.ban/kick` — дефолт причины «Не указана»
### Производительность (наблюдения)
- Команды выполняются за 0–100 мс (virtual threads), CPU ~0.2%, RAM 186 МБ — узких мест нет
- Основная задержка — внешние HTTP API (таймауты 10 с + retry с бэк-оффом); очередь ограничивает параллелизм до 4
### Оптимизации (2026-08-19)
- [x] `AnimalApi` — Caffeine-кэш успешных URL (TTL 30 с): повторные `!neko`/`!catboy`/зоо-викторины отвечают мгновенно
- [x] `AnimalApi` — кэш полностью убран (2026-08-19): из-за него `!neko` 30с отдавал одну и ту же фотку
- [x] `MemeCommand` — HttpURLConnection (без таймаутов, мог висеть) → OkHttp 6с/8с
- [ ] `Database` — включить `PRAGMA journal_mode = WAL` (сейчас нет — блокировки при конкурентной записи)
- [ ] NekosLifeApi — таймауты/ретраи вынести в конфиг (10с → 8с, retry 3 → 2)
- [ ] Негативный кэш неудачных запросов в AnimalApi (не долбить упавший API 30с)
- [ ] Dockerfile — JVM флаги (`-Xmx512m -XX:+UseG1GC -XX:+UseStringDeduplication`)
- [ ] Разобрать остальные `HttpURLConnection` на общий OkHttp-клиент
---
## 🚧 v2.5 — Java 25, рейтинги, память (2026-08-19)
### 1. Java 26 + Gradle 9.7.0
- [x] `build.gradle`: `toolchain { languageVersion = 26 }` — Gradle сам находит JDK 26 (`/usr/lib/jvm/java-26-openjdk`), `export JAVA_HOME` больше не нужен
- [x] `.java-version` (файл `26`) — для IDE/jenv/asdf
- [x] `gradle/wrapper/gradle-wrapper.properties`: 9.3.1 → 9.7.0 (системный gradle 9.7.0 в `/usr/bin/gradle`)
- [x] `Dockerfile`: `eclipse-temurin:21-jdk-alpine` → `26-jdk-alpine`, `21-jre-alpine` → `26-jre-alpine`
- [x] Mockito 5.15.2 тянет ByteBuddy 1.14.x (не знает класс-файлы major 70) → добавить `byte-buddy:1.18.8` (уже в кэше, поддержка Java 26)
- [x] CI workflows (ci/docker-publish/release): `java-version: "21"` → `"26"`
- [x] Пересобрать локально + `spotlessApply` (Spotless 6.25→8.10.0, SpotBugs 4.9.3→4.10.3 для major 70; 458 тестов ✅)
### 1.1 Обновление зависимостей (2026-08-19, все 20)
- [x] jda 6.4.2→6.5.0, lavaplayer 2.2.6→2.2.7, logback 1.5.18→1.6.3, caffeine 3.2.0→3.2.4
- [x] sqlite 3.49.1.0→3.53.2.1, **hikaricp 6.3.0→7.1.0** (мажор, API совместим, тесты ✅), okhttp 5.4.0→5.5.0
- [x] junit 5.12.1→5.13.4, junit-platform-launcher 1.12.1→1.13.4, mockito 5.15.2→5.23.0, bytebuddy 1.18.8→1.18.12
- [x] **slf4j 2.1.0-alpha1 → 2.0.18** (стабильный вместо alpha)
- [x] checkstyle 10.23.0→13.10.0 (toolVersion, требует Java 21+ ✓), spotbugsPlugin 6.1.7→6.4.8, jsoup 1.19.1→1.22.2
- [x] gson 2.14.0, guice 7.0.0, lavalinkClient 3.4.0, dotenv 3.2.0, spotbugs 4.10.3 — актуальные, без изменений
### 2. `!generateratings` генерит только на 2 человек
- [x] **Причина**: JDA `MemberCachePolicy.DEFAULT` кэширует только голосовых/owner/self + `ChunkingFilter` по умолчанию чанкует только гильдии ≥250 участников → `guild.getMembers()` = 2
- [x] `core/JdaFactory.java`: `.setMemberCachePolicy(MemberCachePolicy.ALL)` + `.setMemberChunkingFilter(ChunkingFilter.ALL)`
- [x] `GenerateRatingsCommand.java`: `guild.getMembers()` → `guild.loadMembers().get()` (подстраховка)
### 3. Команда топа/низа рейтингов
- [x] `RatingsRepository`: `getTopRatings(guildId, type, limit)` + `getBottomRatings(...)` (ORDER BY value DESC/ASC LIMIT)
- [x] Новая команда `feature/rating/commands/board/RatingBoardCommand` — флейворы `ratingstop`/`ratingbottom` (высшие/низшие), рендер `<@id> — value` без резолва участников, медали топ-3
- [x] Зарегистрировать в `feature/utility/UtilityCommandRegistrar` (рядом с GenerateRatingsCommand)
### 4. Лимит памяти 1 ГБ
- [x] `docker-compose.yml`: сервис `bot` → `mem_limit: 1g`
- [x] `Dockerfile` ENTRYPOINT: JVM-флаги `-Xmx512m -XX:+UseG1GC -XX:+UseStringDeduplication`
### 5. Дальнейшие оптимизации (поиск)
- [x] Изучено: Gradle 9.4+ нужен для Java 26 (9.7.0 ✓); Gradle 8.14.3 падает на major 69/70; JDA — `loadMembers()` требует GUILD_MEMBERS intent (у бота включён)
- [ ] `Database`: `PRAGMA journal_mode = WAL`
- [ ] NekosLifeApi: таймауты/ретраи в конфиг
- [ ] Негативный кэш в AnimalApi
- [ ] Свести все HttpURLConnection на OkHttp
---
## 🔴 Результаты аудита KISS/DRY/SOLID (2026-07-04)
### DRY — дублирование кода (критично)
| # | Что дублируется | Где | Сколько раз | Статус |
|---|----------------|-----|-------------|--------|
| D1 | `getDeterministicValue()` — целиком | GayCommand, FatCommand, IqCommand, PpCommand, BoykisserCommand, MustacheCommand, RateCommand | 7 | ✅ RatingService |
| D2 | Cooldown check boilerplate (5 строк) | Все entertainment команды | 10+ | ✅ RatingService |
| D3 | `trunc()` / `err()` helpers | MuteCommand, WarnCommand, SwapAndCaseCommand, UnmuteCommand, PunishCommand | 5 | ✅ ModerationHelper |
| D4 | Voice connect retry loop (цикл 10×200ms) | PlayCommand, RadioManager | 2 | ✅ VoiceHelper.connectForPlayback() |
| D5 | RatingConfig — 6 методов с идентичной структурой | RatingConfig.java | 6 | ✅ Data-driven getLevels(name) + findLevel(value, name) |
| D6 | Social команды (hug/kiss/slap/poke/tickle/cuddle/feed/pat) — шаблон | 10 файлов | 10 | ✅ SocialCommand.Flavor (7 команд), pat/compliment/roast — отдельные |
### SOLID
| # | Нарушение | Файл | Описание |
|---|-----------|------|----------|
| S1 | SRP | BotModule.java | Биндит всё приложение | ✅ Перенесён в feature модули + LifecycleModule |
| S2 | SRP | SwapAndCaseCommand.java | Две разные команды в одном файле | ✅ Уже разделены: SwapCommand + CaseLookupCommand |
| S3 | SRP | BotInitializer.java | Инициализирует 3 JDA, Lavalink, mutes, counters, health | ✅ JdaFactory + LavalinkManagerFactory |
| S4 | SRP | ShutdownManager.java | 9 зависимостей в конструкторе | ✅ Lifecycle interface, 3 зависимости |
| S5 | SRP | PlayCommand.java | execute() на 145 строк | ✅ PlayLoadHandler + VoiceHelper.connectForPlayback() |
| S6 | OCP | Meter-команды (gay/fat/iq/pp) | Добавить новую = копировать 90 строк | ✅ RatingService |
| S7 | DIP | MuteCommand, WarnCommand | >5 конкретных репозиториев вместо интерфейсов | ✅ Репозитории уже интерфейсы (ModerationCaseRepository, ActiveMuteRepository, WarningRepository) |
### Файлы >150 строк
| # | Файл | Строк |
|---|------|-------|
| F1 | PlayCommand.java | 148 ✅ |
| F2 | QueueSlashHandler.java | 207 |
| F3 | MusicBotControlHandler.java | 207 |
| F4 | QueueCommands.java | 207 |
| F5 | TrackScheduler.java | 113 ✅ |
| F6 | MuteCommand.java | 173 ✅ |
| F7 | ModerationLogger.java | 199 |
| F8 | FeatureTables.java | 193 |
| F9 | DatabaseMigration.java | 193 |
| F10 | VoiceChannelManager.java | 187 |
| F11 | ServerStatsService.java | 187 |
| F12 | VoiceSetupPrefixCommand.java | 183 |
| F13 | RadioManager.java | 182 |
| F14 | MetricsCollector.java | 182 |
| F15 | WarnCommand.java | 184 |
| F16 | BotInitializer.java | 135 ✅ |
| F17 | SlashHandler's (Queue/Filter) | >100 |
| F18 | GenerateRatingsCommand.java | 172 |
| F19 | BlackjackService.java | 172 |
| F20 | SqlEconomyRepository.java | 169 |
| F21 | GuildMusicManager.java | 167 |
| F22 | ProfileCommand/RankCardGen | >150 |
### >3 файла в папке
| # | Папка | Файлов |
|---|-------|--------|
| P1 | feature/entertainment/commands/ | 15 |
| P2 | feature/social/commands/ | 10 |
| P3 | feature/economy/command/minigame/ | 7 |
| P4 | feature/level/ (все субпапки) | 21 |
| P5 | feature/moderation/ (все субпакеты) | 18 |
| P6 | feature/music/ (все субпакеты) | 22 |
| P7 | feature/voice/ (все субпакеты) | 12 |
### Нет тестов
| # | Фича | Файлов |
|---|------|--------|
| T1 | music | 20+ |
| T2 | voice | 12 |
| T3 | social | 10 |
| T4 | role | 9 |
| T5 | radio | 5 |
| T6 | reputation | 4 |
| T7 | counter | 7 |
| T8 | admin | 1 |
| T9 | help | 2 |
| T10 | utility | 2 |
| T11 | welcome | 2 |
| T12 | profile | 5 |
| T13 | serverstats | 1 |
## 📋 План рефакторинга (KISS/DRY/SOLID)
### Этап 8: Убить дублирование (DRY) — ✅ завершён
- [x] D1/D2: `RatingService` с `getDeterministicValue()`, cooldown, member parsing — 7 команд переписаны
- [x] D3: `ModerationHelper` с `trunc()`/`err()` — MuteCommand, UnmuteCommand, WarnCommand, SwapAndCaseCommand, PunishCommand
- [x] D4: Вынести voice connect retry в утилиту — ✅ VoiceHelper.connectForPlayback()
- [x] D5: RatingConfig — заменить 6 методов на data-driven (Map или config) — ✅ getLevels(name) + findLevel(value, name)
- [x] D6: Social команды — единый `SocialCommand` с параметром action — ✅ SocialCommand.Flavor (7 команд)
### Этап 9: SOLID — ✅ завершён
- [x] S1: Разбить `BotModule` на per-feature модули (FeatureModule → install в BotModule) — ✅ 8 feature модулей + LifecycleModule
- [x] S2: Разделить `SwapAndCaseCommand` на два отдельных файла — ✅ Already split (SwapCommand + CaseLookupCommand)
- [x] S3: Выделить `MusicBotFactory`, `RadioBotFactory` из `BotInitializer` — ✅ JdaFactory + LavalinkManagerFactory
- [x] S4: Уменьшить кол-во зависимостей `ShutdownManager` через события (EventBus) — ✅ Lifecycle interface (9→3 deps)
- [x] S5: Разбить `PlayCommand.execute()` на методы — ✅ PlayLoadHandler + VoiceHelper.connectForPlayback()
- [x] S6: Meter-команды сделать data-driven через `RatingService` — ✅ RatingConfig.findLevel(value, name)
- [x] S7: Выделить интерфейсы для репозиториев/сервисов (DIP) — ✅ Already interfaces
### Этап 10: Файлы ≤150 строк — в процессе
- [x] F01: PlayCommand.java — разбить на методы/классы — ✅ 207→148 строк
- [ ] F02-04: QueueSlashHandler, MusicBotControlHandler, QueueCommands — вынести общие части
- [x] F05: TrackScheduler.java — вывести FilterState в отд. файл или утилиту — ✅ 146→113 строк (TrackStarter)
- [x] F06: MuteCommand.java — разбить на subcommand handler'ы — ✅ 199→173 строки
- [ ] F07: ModerationLogger.java — разбить по типам событий
- [ ] F08-09: FeatureTables, DatabaseMigration — разбить по фичам
- [ ] F10-12: Voice* — вынести логику в сервисы
- [ ] F13: RadioManager.java — вынести voice connect в утилиту
- [ ] F14: MetricsCollector.java — разбить по типам метрик
- [ ] F15: WarnCommand.java — вынести auto-mute в отдельный сервис
- [x] F16: BotInitializer.java — вынести фабрики ботов — ✅ 162→135 строк
- [x] F02-04: QueueSlashHandler, MusicBotControlHandler, QueueCommands — вынести общие части — ✅ вынесены при реорге music
- [ ] Остальные: разбить по SOLID
### Этап 11: ≤6 файлов в папке
- [x] P1: Entertainment команды — уже сгруппированы (choice/, random/; rating-команды в feature/rating/)
- [x] P2: Social команды — уже объединены (SocialCommand + 3 отдельных), 4 файла
- [x] P4: Level — уже разбит на commands/, service/, repository/, domain/
- [x] P5: Moderation — уже разбит по поддоменам (punish, utils, warn, caselookup, log, repository)
- [x] P6: Music — ✅ commands/filter|control|queue, listener/bot|select, manage/player|filter, util/embed|voice|query (все папки ≤6)
### Этап 12: Тесты
- [ ] T1: Music — написать тесты на LavalinkManager, TrackScheduler, PlayCommand
- [ ] T2: Voice — VoiceChannelManager, VoiceButtonHandler
- [ ] T3: Social — интеграционные тесты с NekosLifeApi (мок)
- [ ] T4: Radio — RadioManager, RadioCommand
- [ ] T5: Role — CustomRoleManager, RoleCommandValidator
- [ ] T6-13: Остальные фичи — минимум smoke test
### Техдолг (баги)
- [ ] B1: EmbedFactoryTest — 2 падающих теста (`withTitle`, `withColor`) — IllegalStateException
- [ ] B2: SlowmodeCommandTest — починен (было 7 ошибок компиляции из-за JDA 6.4 SelfMember)
---
**Версия:** 2.4 | **Последнее обновление:** 2026-07-14 | **Java 21 ✅, God Constructor ✅, Critical баги ✅, Economy ✅, Level Roles ✅, Lavalink Music ✅, SpotBugs ✅, Checkstyle ✅, CI/CD ✅, SOLID ✅, TikTok ✅**
## ✅ Что добавлено в v2.4
### 🎬 TikTok Download Command
- [x] Команда `!tiktok <url>` — скачивание видео из TikTok без watermark
- [x] TikTokApi — сервис для работы с tikwm.com API (OkHttp + Gson)
- [x] Скачивание через virtual threads
- [x] Embed с информацией (title, author, duration, cover)
- [x] Проверка размера файла (< 8MB)
- [x] TikTokModule для DI
### 🔧 Рефакторинг SOLID/DRY
- [x] **S1**: BotModule — CommandRegistrar bindings перенесены в feature модули (8 модулей)
- [x] **S2**: SwapAndCaseCommand — уже разделены (SwapCommand + CaseLookupCommand)
- [x] **S3**: BotInitializer — извлечены JdaFactory + LavalinkManagerFactory (162→135 строк)
- [x] **S4**: ShutdownManager — Lifecycle interface, 9→3 зависимости (118→89 строк)
- [x] **S5**: PlayCommand — извлечён PlayLoadHandler + VoiceHelper.connectForPlayback() (207→148 строк)
- [x] **S7**: DIP — репозитории уже интерфейсы (ModerationCaseRepository, ActiveMuteRepository, WarningRepository)
- [x] **D4**: Voice connect retry — VoiceHelper.connectForPlayback()
- [x] **D5**: RatingConfig — data-driven getLevels(name) + findLevel(value, name)
- [x] **D6**: Social команды — SocialCommand.Flavor (7 команд), pat/compliment/roast — отдельные
- [x] **TrackScheduler** — извлечён TrackStarter (146→113 строк)
- [x] **MuteCommand** — execute() разбит на методы (199→173 строки, execute() 23 строки)
### 📦 Новые файлы
- `feature/tiktok/TikTokApi.java` — API сервис
- `feature/tiktok/TikTokCommand.java` — команда
- `feature/tiktok/TikTokModule.java` — Guice модуль
- `core/Lifecycle.java` — интерфейс для shutdown
- `core/JdaFactory.java` — фабрика JDA
- `core/LavalinkManagerFactory.java` — фабрика LavalinkManager
- `core/LifecycleModule.java` — Multibinder для Lifecycle
- `feature/entertainment/EntertainmentModule.java` — CommandRegistrar binding
- `feature/utility/UtilityModule.java` — CommandRegistrar binding
- `feature/music/play/PlayLoadHandler.java` — обработчик загрузки треков
- `feature/music/manage/TrackStarter.java` — retry логика запуска треков
---
## ✅ Что добавлено в v2.3
### 🎵 Lavalink Music Service
- [x] Node.js сервис с 4 Discord ботами (kazagumo + shoukaku)
- [x] Поддержка YouTube, SoundCloud, радио (`!radio <url>`)
- [x] Команды: play, stop, skip, queue, np, volume, pause, resume, shuffle, repeat, clear, move
- [x] Lavalink контейнер с healthcheck
- [x] docker-compose с интеграцией в основной проект
- [x] `.env.example` с 4 токенами
### 🔧 SpotBugs (все реальные баги исправлены)
- [x] FS (7): `\n` → `%n` в MetricsCollector, ServerStatsService
- [x] DMI (6): Random → static final Random в entertainment командах
- [x] SF (4): missing default → добавлены default в switch
- [x] RCN (3): Redundant nullcheck удалены
- [x] RV (1): Bad abs(hashCode) исправлен
- [x] Dm (2): Стандартная кодировка → UTF-8
- [x] OBL/ODR: Database.prepare() закрытие connection
- [x] REC: RankCardGenerator — узкий catch
- [x] DLS: FishingService — dead store удалён
- [x] EI2/EI (96): excluded как DI false positives
- [x] `ignoreFailures = false`
### 🔍 Checkstyle
- [x] Все 9 нарушений исправлены
- [x] `maxErrors`/`maxWarnings` удалены (0 нарушений)
### 🧪 Тесты
- [x] BalanceCommandTest (3 теста)
- [x] DailyCommandTest (2 теста)
- [x] PayCommandTest (3 теста)
- [x] CoinCommandTest (2 теста)
- [x] DiceCommandTest (2 теста)
- [x] EightBallCommandTest (2 теста)
- [x] Всего: 51 файл тестов, ~421+ тестов
### 🚀 CI/CD
- [x] Docker publish workflow (ghcr.io)
- [x] Release workflow (GitHub Releases + JAR artifact)