# 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 ` для скачивания видео | 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` | #### 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`) ### Этап 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 `) - Дилер стоит на 16, ничья = проигрыш, BJ = ×1.5 - Кнопки Hit/Stand **Слоты** (`!slot `) - 🍒🍒🍒 ×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 ` — расширить мем по сабреддиту --- ## ⚡ Стабильность и скорость (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 ` — скачивание видео из 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 `) - [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)