From 4cbbb36159d642a6e88d1a79cc045ac44e8a244a Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 30 Aug 2026 00:29:13 +0200 Subject: [PATCH] fix(lavalink): drop spotify-tokener, lavasrc uses client credentials - spotify URL resolution was broken (500 NPE): preferPartnerApi + customTokenEndpoint depended on dead spotify-tokener (-32000) - remove tokener service from compose, remove dead SPOTIFY_* env from bot container (bot code never used them) - spotify metadata now resolves via standard client credentials, playback is mirrored to SoundCloud via scsearch providers (ISRC/QUERY) --- TODO.md | 57 ++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 14 ---------- lavalink/application.yml | 2 -- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/TODO.md b/TODO.md index 573f04e..0a0aa09 100644 --- a/TODO.md +++ b/TODO.md @@ -37,6 +37,8 @@ | 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`) | --- @@ -85,6 +87,61 @@ | 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` | + --- ## 📋 План рефакторинга diff --git a/docker-compose.yml b/docker-compose.yml index a465323..400497c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,8 +3,6 @@ services: image: ghcr.io/lavalink-devs/lavalink:4-alpine container_name: loki-lavalink restart: unless-stopped - depends_on: - - spotify-tokener ports: - "127.0.0.1:2333:2333" volumes: @@ -22,16 +20,6 @@ services: retries: 3 start_period: 20s - spotify-tokener: - image: ghcr.io/topi314/spotify-tokener:master - container_name: spotify-tokener - restart: unless-stopped - environment: - - SPOTIFY_TOKENER_ADDR=0.0.0.0:8086 - - SPOTIFY_TOKENER_LOG_LEVEL=INFO - networks: - - default - bot: build: . container_name: loki-bot @@ -54,8 +42,6 @@ services: LAVALINK_HOST: lavalink LAVALINK_PORT: "2333" LAVALINK_PASSWORD: Hch4HiVCxkKs9JPiSnrt4Q - SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID} - SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET} R34_API_KEY: ${R34_API_KEY} R34_USER_ID: ${R34_USER_ID} DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} diff --git a/lavalink/application.yml b/lavalink/application.yml index 56ce9e7..d421180 100644 --- a/lavalink/application.yml +++ b/lavalink/application.yml @@ -28,8 +28,6 @@ plugins: countryCode: "RU" playlistLoadLimit: 10 albumLoadLimit: 10 - preferPartnerApi: true - customTokenEndpoint: "http://spotify-tokener:8086/api/token" applemusic: countryCode: "RU" mediaType: "music"