Files
stoat-mbot/README.md
T
Leonid PershinandClaude Opus 5 62d5291cd9 Run the panel as its own compose project behind the host Caddy
The instance's internal Caddy is only for Stoat itself, so the panel no
longer goes through it: the bot ships its own compose project, publishes
3005 on loopback, and the host's external Caddy gives it a domain.

extra_hosts pins the instance domain to host-gateway, so the bot reaches
the API, gateway and LiveKit through the external proxy with a valid
certificate instead of depending on router NAT loopback. The old
in-project layout stays available as a fallback example, together with a
step-by-step guide for when voice fails to connect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:16:04 +03:00

183 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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.
# 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`): бот ведёт себя как обычный клиент —
ходит в API, gateway и LiveKit через тот же внешний прокси с валидным TLS, что и браузеры.
### 4. Указать домен инстанса в `compose.yml`
Бот запускается **отдельным compose-проектом** и с внутренним Caddy инстанса никак не связан.
В [compose.yml](compose.yml) поправьте одну строку — домен вашего Stoat:
```yaml
extra_hosts:
- "chat.example.com:host-gateway"
```
Эта запись заставляет контейнер резолвить домен инстанса в сам хост, где внешний Caddy держит
443 с валидным сертификатом. Так бот не зависит от NAT loopback на роутере.
### 5. Повесить панель на внешний Caddy
Порт панели публикуется только на локальный интерфейс (`127.0.0.1:3005`), домен выдаёт внешний
Caddy хоста — в `/etc/caddy/Caddyfile` ([deploy/Caddyfile.snippet](deploy/Caddyfile.snippet)):
```caddyfile
music.example.com {
reverse_proxy 127.0.0.1:3005
}
```
Затем `sudo systemctl reload caddy`. WebSocket проксируется автоматически.
### 6. Запустить
```bash
cd /opt/stoat-mbot && docker compose up -d --build && docker compose logs -f
```
В логах должно появиться `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 перестановка треков стрелками,
клик по полосе прогресса — перемотка, слайдер громкости, выбор голосового канала, история.
---
## Если бот не заходит в голосовой канал
Порядок диагностики — сверху вниз, каждый шаг отсекает свой слой:
1. **`bot is ready` в логах.** Нет — проблема в `STOAT_API_URL`/`STOAT_BOT_TOKEN`. Проверьте, что
домен из `extra_hosts` совпадает с доменом в `STOAT_API_URL`, и что изнутри контейнера он
резолвится в хост: `docker compose exec mbot node -e "fetch(process.env.STOAT_API_URL).then(r=>console.log(r.status))"`.
2. **`joining voice channel`, но нет `voice connection established`.** Значит `join_call` отдал
токен, а WebSocket до LiveKit не поднялся — смотрите, доступен ли `/livekit` через внешний домен.
3. **Бот в канале, но звука нет.** Это уже медиа-трафик: LiveKit анонсирует клиентам свой адрес
из `rtc.node_ip` / `use_external_ip` в `/opt/stoat/livekit.yml` и ждёт UDP на 50000-50100.
Если анонсируется внешний IP, а роутер не умеет NAT loopback, пакеты от контейнера до него не
дойдут. Тогда либо включите hairpin на роутере, либо запустите бота внутри compose-проекта
Stoat — пример в [deploy/compose.stoat-network.yml.example](deploy/compose.stoat-network.yml.example).
## Настройки
Все параметры — в [.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).