A search reply carried ten markdown links, and Stoat expands every link
into a full-size player — the result was a wall of embeds burying the
list. Stoat's embed generator skips links inside code spans or angle
brackets, so links now go out quietly, and list lines carry no links at
all: title, artist, length, source. Only `!nowplaying` keeps a preview,
where it was asked for.
Choosing a track no longer needs a second command: the results message
gets 1️⃣–5️⃣ reactions and picking one queues the track, with `!pick`
still there when reactions fail or five results are not enough. Only the
person who searched can pick, so a list cannot be hijacked.
Also `!playvideo` to turn video on and queue a track in one go, and
setVideo no longer requires a running player — the switch is a setting,
and demanding a player made it fail on a server nothing had played on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Switching tracks killed the process: revoice's #cleanUp() dereferences
this.fProc unconditionally, and its own ffmpeg error handler calls stop()
a second time after stop() has already nulled that field. Killing ffmpeg
is what triggers that error, so its handlers are detached first, the
instance's stop() is wrapped defensively (revoice calls it internally),
and an uncaughtException handler keeps the bot in the channel if the
dependency throws from another async callback.
Failed command-message deletions were logged at debug, i.e. invisible in
the default configuration; they now warn with Stoat's error type, and the
README says which permission the bot's role needs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
revoice.js reads `room.isConnected()`, but @livekit/rtc-node turned that
into a getter, so every join threw "this.room.isConnected is not a
function" before the bot ever reached the channel. The player no longer
touches that getter: readiness comes from the connection's own join /
roomfetched events and is cleared when it reports the offline state.
Also delete the invoking chat message once a command is recognised
(DELETE_COMMAND_MESSAGES, on by default) so channels stay readable;
failures are non-fatal since it needs ManageMessages.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>