ksan 5068e40778
CI/CD / build (push) Successful in 47s
CI/CD / deploy (push) Successful in 16s
updated readme
2026-08-31 15:58:38 +02:00
2026-07-07 15:18:13 +02:00
2026-07-07 15:18:13 +02:00
2026-07-10 00:13:57 +02:00
2026-07-07 15:18:13 +02:00
2026-08-30 19:21:25 +02:00
2026-07-10 00:13:57 +02:00
2026-07-07 15:18:13 +02:00
2026-07-07 15:18:13 +02:00
2026-07-07 13:19:53 +00:00
2026-08-15 17:22:51 +02:00
2026-08-31 15:58:38 +02:00
2026-07-07 15:18:13 +02:00

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 90100%. 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 pass lavalink explicitly.

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 bot and applications.commands scopes (and the Connect + Speak voice permissions)
  • Enable Developer Mode in Discord and copy your server's Guild ID
  • cp .env.example .env and fill in BOT_TOKEN, LAVALINK_PASSWORD (and GUILD_ID)
  • Start Lavalink only: docker compose up -d lavalink and 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 /hello to confirm it responds, then /play from 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.

  1. 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_PASSWORD
    

    Make sure catchphrases.txt, always100.txt, and always90.txt exist 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.

  2. 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-plugin version in lavalink/application.yaml and 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-plugins named Docker volume. If a plugin bump doesn't seem to take effect, docker compose down -v clears it — that also drops the volume, so the next start re-downloads everything.
S
Description
A hobby Discord bot for personal use.
Readme MIT 906 KiB
Languages
Java 97.2%
Dockerfile 2.8%