Add music bot for self-hosted Stoat with web control panel

Plays audio into Stoat voice channels over LiveKit and exposes the same
player through both chat commands and a browser panel, so the two never
drift apart: everything routes through a single MusicManager.

- core: per-server GuildPlayer (queue, loop, shuffle, seek, volume,
  idle auto-leave) driving revoice.js/@livekit/rtc-node and ffmpeg
- sources: yt-dlp for YouTube/SoundCloud, direct media URLs and internet
  radio, optional local library with path-traversal guards
- bot: 18 chat commands with aliases, plus !panel one-time login links
- api: Fastify REST + WebSocket, sessions authenticated against the
  instance's own /auth/session/login (TOTP supported), permissions
  re-checked against Stoat membership and roles on every request
- web: React panel with search, queue editing, seek and volume
- deploy: Dockerfile, compose.override.yml and Caddyfile snippets for
  dropping the service into an existing /opt/stoat stack

Verified with npm run typecheck, both builds, and scripts/smoke-api.mjs
(9 API checks). Voice playback itself needs a live instance to test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-09-08 23:12:43 +03:00
co-authored by Claude Opus 5
parent d9d0e9f6bf
commit a9b7ccdd16
43 changed files with 12436 additions and 0 deletions
+177
View File
@@ -1,2 +1,179 @@
# stoat-mbot
Музыкальный бот для self-hosted [Stoat](https://github.com/stoatchat/self-hosted) с веб-панелью:
поиск и воспроизведение в голосовых каналах, очередь, перемотка, громкость — из чата и из браузера,
состояние синхронизировано в обе стороны через WebSocket.
**Источники:** YouTube, SoundCloud, прямые ссылки и интернет-радио, локальная медиатека (опционально).
**Голос:** LiveKit — тот же, что и в вашем инстансе (`revoice.js` + `@livekit/rtc-node`), звук готовит `ffmpeg`.
**Вход в панель:** учётными записями вашего же Stoat (пароль уходит прямо в `/auth/session/login`
вашего инстанса, бот его не хранит) либо одноразовой ссылкой по команде `!panel`.
---
## Как это устроено
```
Stoat (чат) ──messageCreate──► bot/commands ─┐
├──► MusicManager ──► GuildPlayer ──► LiveKit
Браузер ──REST + WebSocket──► api/server ───┘ (очередь, (ffmpeg,
громкость, yt-dlp)
повтор)
```
Чат-команды и панель дергают один и тот же `MusicManager`, поэтому «нажал в браузере — увидел в чате»
работает без рассинхрона. Каждое действие панели повторно проверяет членство и права в Stoat,
так что доступ живёт в ролях Stoat, а не в отдельной базе бота.
| Слой | Файлы |
| --- | --- |
| Источники и yt-dlp | [src/sources](src/sources) |
| Плеер и очередь | [src/core/player.ts](src/core/player.ts), [src/core/manager.ts](src/core/manager.ts) |
| Чат-бот | [src/bot](src/bot) |
| REST + WebSocket | [src/api/server.ts](src/api/server.ts) |
| Веб-панель (React) | [web/src](web/src) |
---
## Установка рядом со Stoat
Предполагается раскладка `/opt/stoat` (инстанс) и `/opt/stoat-mbot` (этот репозиторий).
### 1. Клонировать репозиторий
```bash
cd /opt && git clone https://gitea.hsrv.site/mrleo1nid/stoat-mbot.git stoat-mbot
```
### 2. Создать бота в Stoat
Settings → **My Bots** → создать бота → скопировать токен → пригласить бота на сервер.
Боту нужны права: читать сообщения, писать сообщения и подключаться к голосовым каналам.
### 3. Заполнить `.env`
```bash
cd /opt/stoat-mbot
cp .env.example .env
openssl rand -hex 32 # → JWT_SECRET
```
Минимум, что нужно указать:
```dotenv
STOAT_API_URL=https://stoat.example.com/api
STOAT_BOT_TOKEN=<токен бота>
PUBLIC_URL=https://music.example.com
JWT_SECRET=<случайные 32 байта>
```
`STOAT_API_URL` лучше указывать публичный (`https://домен/api`): бот тогда ведёт себя как обычный
клиент и получает от `join_call` тот же LiveKit-URL, что и все. Внутренний `http://api:14702` тоже
работает, но убедитесь, что LiveKit-URL из ответа резолвится изнутри контейнера.
### 4. Добавить сервис в `compose.override.yml`
В `/opt/stoat/compose.override.yml` (готовый пример — [deploy/compose.override.yml.example](deploy/compose.override.yml.example)):
```yaml
services:
caddy:
ports: !override
- "127.0.0.1:8880:80"
mbot:
build: ../stoat-mbot
restart: always
env_file: ../stoat-mbot/.env
depends_on:
api:
condition: service_started
volumes:
- ../stoat-mbot/data:/data
```
Сервис попадает в тот же compose-проект и ту же сеть, поэтому Caddy видит его как `http://mbot:3005`,
а бот ходит в Stoat API по имени `api`.
### 5. Пробросить панель через Caddy
В `/opt/stoat/Caddyfile` добавьте блок ([deploy/Caddyfile.snippet](deploy/Caddyfile.snippet)):
```caddyfile
http://music.example.com {
reverse_proxy http://mbot:3005
}
```
Схема `http://` — потому что у вас TLS терминирует внешний прокси (Caddy слушает `127.0.0.1:8880`).
WebSocket проксируется автоматически.
### 6. Запустить
```bash
cd /opt/stoat && docker compose up -d --build mbot && docker compose logs -f mbot
```
В логах должно появиться `yt-dlp detected`, `bot is ready` и `panel is listening`.
---
## Использование
В чате (префикс по умолчанию `!`):
| Команда | Что делает |
| --- | --- |
| `!play <ссылка или название>` | добавить трек/плейлист в очередь |
| `!playnext`, `!playnow` | следующим / немедленно |
| `!search <запрос>``!pick <n>` | поиск с выбором из списка |
| `!skip [n]`, `!stop`, `!pause`, `!resume` | управление воспроизведением |
| `!queue [страница]`, `!nowplaying` | очередь и текущий трек |
| `!volume [0-200]`, `!loop [off\|track\|queue]`, `!shuffle` | звук и порядок |
| `!remove <n>`, `!clear`, `!seek 1:23` | правка очереди и перемотка |
| `!join`, `!leave` | зайти в ваш голосовой канал / выйти |
| `!panel` | личная ссылка на веб-панель (действует 10 минут) |
| `!help` | список команд |
Префиксы поиска: `sc:` — SoundCloud, `yt:` — YouTube, `local:` — локальная медиатека.
В панели: поиск с добавлением в очередь/следующим/сейчас, drag-free перестановка треков стрелками,
клик по полосе прогресса — перемотка, слайдер громкости, выбор голосового канала, история.
---
## Настройки
Все параметры — в [.env.example](.env.example). Что стоит знать:
- `REQUIRE_DJ_ROLE=true` — управлять смогут только владелец сервера, обладатели `ManageServer`
и роли из `DJ_ROLE_NAME`. По умолчанию `false`: играть может любой участник сервера.
- `IDLE_TIMEOUT_SECONDS` — через сколько секунд простоя (или пустого канала) бот выходит из войса.
- `LOCAL_MEDIA_DIR` — примонтируйте том с музыкой и укажите путь внутри контейнера,
тогда заработают `local:` и поиск по медиатеке.
- `YTDLP_COOKIES` — путь к `cookies.txt`, если YouTube просит подтверждения возраста или логина.
## Разработка
```bash
npm install && npm --prefix web install
cp .env.example .env # STOAT_API_URL можно указать публичный адрес инстанса
npm run dev # бот + API на :3005
npm run web:dev # панель на :5180 с проксированием на :3005
```
Проверки: `npm run typecheck`, `npm run build`, `npm --prefix web run build`.
Локально нужен `yt-dlp` в `PATH` (или укажите `YTDLP_PATH`); `ffmpeg` приезжает с `ffmpeg-static`.
## Ограничения
- `revoice.js` — сторонняя библиотека без ретраев на разрыв LiveKit-соединения; при падении войса
бот выходит из канала, повторный `!join` восстанавливает работу.
- Перемотка для YouTube/SoundCloud перезапускает поток с нужной позиции (одна лишняя обращение к
yt-dlp), для локальных файлов и прямых ссылок — мгновенная.
- Скачивание с YouTube формально противоречит его ToS; используйте на своё усмотрение,
для «чистого» сценария есть SoundCloud, прямые ссылки и локальная медиатека.
## Лицензия
MIT — см. [LICENSE](LICENSE).