Files
stoat-mbot/README.md
T
Leonid PershinandClaude Opus 5 1ac99b4a10 Document cloning into /opt as root-owned directory
Cloning straight into /opt fails with "Permission denied", and a clone
left owned by root would also make the ./data bind mount unwritable for
the container's node user.

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

193 lines
11 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. Клонировать репозиторий
`/opt` принадлежит root, поэтому клонируем под `sudo` и сразу возвращаем владение себе:
```bash
sudo git clone https://gitea.hsrv.site/mrleo1nid/stoat-mbot.git /opt/stoat-mbot
```
```bash
sudo chown -R $USER:$USER /opt/stoat-mbot
```
`chown` здесь не косметика: дальше вы правите `.env` и `compose.yml`, а bind-mount `./data`
иначе создастся от root. Контейнер работает под пользователем `node` (uid 1000) — если ваш
пользователь тоже uid 1000 (`id -u`), права на `data/` совпадут и yt-dlp сможет туда писать.
### 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).