Botinator
A hobby Discord bot for personal use. It's a small JDA bot with a few fun slash commands and music playback powered by Lavalink.
Personal project — not intended as a polished, general-purpose bot.
Commands
| Command | What it does |
|---|---|
/hello |
Sanity-check greeting |
/say <message> |
Bot repeats your message |
/rolldice [sides] |
Roll a die (default 6 sides) |
/armeter |
Measures your "AR" percentage (just for laughs) |
/play <query> |
Play or queue a track/playlist by URL, or search text (YouTube) |
/skip |
Skip the current track |
/queue |
Show the queue as a paginated embed (◀/▶ buttons) |
/loop [queue|song|off] |
Loop playback. Bare /loop toggles looping the whole queue; song loops just the current track; off stops looping |
/clear |
Clear the queue (keeps the current track playing) |
How it works
The bot is just a Lavalink client — actual audio is streamed by a separate
Lavalink v4 server that runs alongside it (started with Docker). YouTube support
comes from the youtube-plugin, configured in lavalink/application.yaml.
- Bot: Java + Gradle (JDA 6,
lavalink-client) - Music server: Lavalink v4 via
docker-compose.yaml
Requirements
- A JDK (builds on Java 17+; CI and the Docker image use Java 25)
- Docker + Docker Compose (to run the Lavalink server)
- A Discord bot token
Configuration
Copy the example env file and fill in your values:
cp .env.example .env
| Variable | Required | Description |
|---|---|---|
BOT_TOKEN |
✅ | Discord bot token (Developer Portal → your app → Bot) |
GUILD_ID |
optional | Test server ID for instant command registration. If unset, commands register globally (can take up to an hour) |
LAVALINK_URI |
optional | Defaults to ws://localhost:2333 for local runs. In docker-compose.yaml the bot is pointed at ws://lavalink:2333 (the service name) |
LAVALINK_PASSWORD |
✅ | Shared secret between the bot and the Lavalink server. Compose passes the same value to the server as LAVALINK_SERVER_PASSWORD, so they always match. The bot exits if it's unset |
PLUGINS_YOUTUBE_OAUTH_REFRESHTOKEN |
optional | Read by the Lavalink container, not the bot. Skips the Google device flow on restart — see YouTube tracks that need a login |
CATCHPHRASES_FILE |
optional | Path to the catchphrase list /play quotes from. Defaults to catchphrases.txt in the working directory |
ALWAYS_100_USERS_FILE |
optional | Path to the list of user IDs /armeter always rates 100%. Defaults to always100.txt |
ALWAYS_90_USERS_FILE |
optional | Path to the list of user IDs /armeter always rates 90–100%. Defaults to always90.txt |
All three text files take one entry per line; blank lines and # comments are
ignored, and a missing file is not fatal (the bot just falls back to a default).
.env is gitignored, so your token stays local.
Running
./gradlew run reads real environment variables — it does not auto-load .env.
So load it into your shell first.
# 1. Start ONLY the Lavalink server — not the bot container — so it doesn't clash
# with the bot you're running from source. (First run downloads the YouTube
# plugin; wait for "ready".)
docker compose up -d lavalink
docker compose logs -f lavalink # wait for "Lavalink is ready to accept connections", then Ctrl+C
# 2. Load your .env and run the bot
set -a; source .env; set +a
./gradlew run
docker compose up -d(no service name) would start the bot container too, giving you two bots. For source development, always passlavalinkexplicitly.
When it connects you'll see Bot connected and ready as ... in the console. Then join a
voice channel in your server and try /play <song or url>.
TODO — steps to get it running
- Install a JDK (17+) and Docker
- Create a Discord application + bot, and copy the bot token
- Invite the bot to your server with the
botandapplications.commandsscopes (and the Connect + Speak voice permissions) - Enable Developer Mode in Discord and copy your server's Guild ID
cp .env.example .envand fill inBOT_TOKEN,LAVALINK_PASSWORD(andGUILD_ID)- Start Lavalink only:
docker compose up -d lavalinkand wait until the logs say it's ready - Load env vars:
set -a; source .env; set +a - Run the bot:
./gradlew run - In Discord, run
/helloto confirm it responds, then/playfrom a voice channel
Run with Docker (prebuilt image)
If you'd rather not build from source, docker-compose.yaml pulls the prebuilt
bot image and runs it alongside Lavalink. You only need Docker — no JDK.
-
Create your config and the files the bot reads at runtime, next to the compose file:
cp .env.example .env # then fill in BOT_TOKEN and LAVALINK_PASSWORDMake sure
catchphrases.txt,always100.txt, andalways90.txtexist as files here. They're mounted into the container — if any is missing, Docker creates an empty directory in its place and the mount breaks. -
Pull and start both services:
docker compose pull # fetch the bot + Lavalink images docker compose up -d # start in the background docker compose logs -f # watch logs; look for "Bot connected and ready as ..." docker compose down # stop
The bot connects to Lavalink over the internal Docker network automatically, so
you don't set LAVALINK_URI yourself here.
YouTube tracks that need a login
Some tracks — age-restricted ones, and anything YouTube decides to gate behind
"Sign in to confirm you're not a bot" — won't load on the anonymous clients. The
TV client can play them, but only with a linked Google account, so
lavalink/application.yaml enables the plugin's OAuth flow. Use a throwaway
Google account: Google does ban accounts for this.
One-time setup:
docker compose up -d lavalink
docker compose logs -f lavalink
The logs print something like:
OAUTH INTEGRATION: To give youtube-source access to your account, go to
https://www.google.com/device and enter code XXXX-XXXX
Open that page, enter the code, and approve. Lavalink then logs the refresh
token. Put it in .env:
PLUGINS_YOUTUBE_OAUTH_REFRESHTOKEN=<the token from the logs>
and docker compose up -d lavalink again. Compose already passes .env into the
Lavalink container, and the variable name maps onto
plugins.youtube.oauth.refreshToken, so later restarts skip the device flow.
Everything that already worked keeps using the anonymous clients — TV is last
in the client list and only gets used when the others are refused.
Remote cipher
YouTube serves some regions a localized player script the plugin can't parse,
which fails playback with must find sig function. lavalink/application.yaml
therefore hands the signature deciphering to a remote cipher server
(https://cipher.kikkia.dev) instead of doing it locally. It's a third-party
service and only sees the player script, no account data — drop the
remoteCipher block if you'd rather not use it and playback works without it.
Deployment
.gitea/workflows/ci-cd.yaml builds on every push to main/dev, and on main
also pushes the bot image to the private registry and deploys over SSH. The
deploy copies only docker-compose.yaml and lavalink/application.yaml to
the server — .env, catchphrases.txt, always100.txt and always90.txt live
on the server and are never overwritten — then runs docker compose pull && up -d
followed by an explicit restart lavalink and restart bot. Both restarts are
needed: a bind-mounted config change doesn't recreate a service on its own, and
the bot has to reconnect afterwards or every /play fails with
Node main is unavailable.
Secrets used by the workflow: DOMAIN, REGISTRY_USER, REGISTRY_PASSWORD,
SSH_PRIVATE_KEY, DEPLOY_HOST, DEPLOY_USER.
Notes
- Queue state is kept in memory, so it resets if the bot restarts.
- YouTube occasionally breaks playback when Google changes its player. The fix is usually
bumping the
youtube-pluginversion inlavalink/application.yamland restarting the server. - That plugin is currently pinned to a snapshot commit (
snapshot: true), not a tagged release: the TV-client fixes playback depends on aren't in a release yet. Move back to a version number once they are. - Lavalink downloads plugin jars into the
lavalink-pluginsnamed Docker volume. If a plugin bump doesn't seem to take effect,docker compose down -vclears it — that also drops the volume, so the next start re-downloads everything.