Compare commits
8 Commits
7f17a604e5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 64a472c540 | |||
| f70a50c06f | |||
| cab8f32542 | |||
| 25cbe3bd56 | |||
| be360843eb | |||
| d4062f98a3 | |||
| 80719eb253 | |||
| cbefb7b21f |
@@ -0,0 +1,13 @@
|
||||
# Keep the build context small and reproducible — everything the image needs is
|
||||
# rebuilt inside the Dockerfile.
|
||||
.git
|
||||
.gradle
|
||||
build/
|
||||
.idea
|
||||
*.iml
|
||||
.env
|
||||
.env.*
|
||||
lavalink/plugins/
|
||||
catchphrases.txt
|
||||
always100.txt
|
||||
always90.txt
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copy this file to `.env` and fill in your real values.
|
||||
# `.env` is gitignored so your secrets stay local.
|
||||
|
||||
# Your Discord bot token (Developer Portal -> your app -> Bot -> Reset/Copy Token).
|
||||
BOT_TOKEN=your-bot-token-here
|
||||
|
||||
# Numeric ID of your test server, so /hello registers instantly as a guild command.
|
||||
# Enable Developer Mode in Discord, right-click the server, "Copy Server ID".
|
||||
# Leave unset to register /hello globally (can take up to an hour to appear).
|
||||
GUILD_ID=123456789012345678
|
||||
|
||||
# Lavalink server the bot connects to for music playback.
|
||||
# In docker-compose the bot reaches Lavalink at ws://lavalink:2333 (set there);
|
||||
# ws://localhost:2333 is the default for running the bot outside a container.
|
||||
LAVALINK_URI=ws://localhost:2333
|
||||
# Shared secret between the bot and the Lavalink server. docker-compose passes
|
||||
# this same value to the server as LAVALINK_SERVER_PASSWORD, so the two always
|
||||
# match. Set a strong value — do NOT ship the youshallnotpass default.
|
||||
LAVALINK_PASSWORD=change-me
|
||||
|
||||
# Path to the catchphrases file (one phrase per line). Optional; defaults to
|
||||
# catchphrases.txt in the working directory.
|
||||
CATCHPHRASES_FILE=catchphrases.txt
|
||||
@@ -0,0 +1,61 @@
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 25
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew --no-daemon build
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build & push Docker image
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
|
||||
docker login git.${{ secrets.DOMAIN }} \
|
||||
-u ${{ secrets.REGISTRY_USER }} --password-stdin
|
||||
docker build -t git.${{ secrets.DOMAIN }}/${{ secrets.REGISTRY_USER }}/botinator:latest .
|
||||
docker push git.${{ secrets.DOMAIN }}/${{ secrets.REGISTRY_USER }}/botinator:latest
|
||||
|
||||
- name: Deploy to VPS
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/key
|
||||
chmod 600 ~/.ssh/key
|
||||
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
HOST=${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}
|
||||
# Path relative to the remote user's home (ssh/scp start there). Do NOT
|
||||
# use ~ here — it would expand on the runner, not on the server.
|
||||
DIR=programs/botinator
|
||||
|
||||
# Ship the compose file and Lavalink config to the server. .env and
|
||||
# catchphrases.txt live on the server and are intentionally not copied.
|
||||
# Plugins live in a named Docker volume (see docker-compose.yaml), so
|
||||
# only the lavalink/ dir for application.yml is needed here.
|
||||
ssh -i ~/.ssh/key $HOST "mkdir -p $DIR/lavalink"
|
||||
scp -i ~/.ssh/key docker-compose.yaml $HOST:"$DIR/docker-compose.yaml"
|
||||
scp -i ~/.ssh/key lavalink/application.yml $HOST:"$DIR/lavalink/application.yml"
|
||||
|
||||
ssh -i ~/.ssh/key $HOST \
|
||||
"cd $DIR && docker compose pull && docker compose up -d"
|
||||
@@ -54,5 +54,7 @@ lavalink/plugins/
|
||||
### Local secrets ###
|
||||
.env
|
||||
.env.*
|
||||
# ...but keep the committed template.
|
||||
!.env.example
|
||||
*.local
|
||||
secrets.properties
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# ---- Build stage: compile and assemble a runnable distribution ----
|
||||
FROM eclipse-temurin:25-jdk AS build
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the Gradle wrapper and build scripts first so dependency resolution is
|
||||
# cached in its own layer and only re-runs when the build files change.
|
||||
COPY gradlew settings.gradle.kts build.gradle.kts ./
|
||||
COPY gradle ./gradle
|
||||
RUN chmod +x gradlew && ./gradlew --no-daemon dependencies > /dev/null 2>&1 || true
|
||||
|
||||
# Now the source. `installDist` produces build/install/Botinator with a launcher
|
||||
# script and all dependency jars under lib/.
|
||||
COPY src ./src
|
||||
RUN ./gradlew --no-daemon --stacktrace installDist
|
||||
|
||||
# ---- Runtime stage: JRE only, no build tooling ----
|
||||
FROM eclipse-temurin:25-jre
|
||||
WORKDIR /app
|
||||
|
||||
# Run as an unprivileged user.
|
||||
RUN useradd --system --create-home --uid 10001 botinator
|
||||
USER botinator
|
||||
|
||||
# The assembled app (bin/Botinator launcher + lib/*.jar).
|
||||
COPY --from=build --chown=botinator:botinator /app/build/install/Botinator/ ./
|
||||
|
||||
# BOT_TOKEN etc. and the catchphrases file are supplied at deploy time
|
||||
# (env_file / -e for config, a mounted volume for catchphrases.txt).
|
||||
ENTRYPOINT ["./bin/Botinator"]
|
||||
@@ -27,11 +27,11 @@ The bot is just a **Lavalink client** — actual audio is streamed by a separate
|
||||
comes from the `youtube-plugin`, configured in `lavalink/application.yml`.
|
||||
|
||||
- Bot: Java + Gradle (JDA 6, `lavalink-client`)
|
||||
- Music server: Lavalink v4 via `docker-compose.yml`
|
||||
- Music server: Lavalink v4 via `docker-compose.yaml`
|
||||
|
||||
## Requirements
|
||||
|
||||
- A recent JDK (Java 17+; use the latest LTS if the build complains)
|
||||
- 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
|
||||
|
||||
@@ -47,8 +47,8 @@ cp .env.example .env
|
||||
| --- | --- | --- |
|
||||
| `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` (matches `docker-compose.yml`) |
|
||||
| `LAVALINK_PASSWORD` | optional | Defaults to `youshallnotpass` (matches `lavalink/application.yml`) |
|
||||
| `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 |
|
||||
|
||||
`.env` is gitignored, so your token stays local.
|
||||
|
||||
@@ -58,15 +58,20 @@ cp .env.example .env
|
||||
So load it into your shell first.
|
||||
|
||||
```bash
|
||||
# 1. Start the Lavalink server (first run downloads the YouTube plugin; wait for "ready")
|
||||
docker compose up -d
|
||||
docker compose logs -f # wait for "Lavalink is ready to accept connections", then Ctrl+C
|
||||
# 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>`.
|
||||
|
||||
@@ -77,12 +82,39 @@ voice channel in your server and try `/play <song or url>`.
|
||||
- [ ] 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` (and `GUILD_ID`)
|
||||
- [ ] Start Lavalink: `docker compose up -d` and wait until the logs say it's ready
|
||||
- [ ] `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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
## Notes
|
||||
|
||||
- Queue state is kept in memory, so it resets if the bot restarts.
|
||||
|
||||
+39
-6
@@ -1,18 +1,51 @@
|
||||
# Runs a Lavalink v4 server for the bot to connect to.
|
||||
# docker compose up -d # start in background
|
||||
# docker compose logs -f # watch logs
|
||||
# docker compose down # stop
|
||||
# Runs the bot together with a Lavalink v4 server for music playback.
|
||||
# docker compose up -d --build # build the bot image locally and start
|
||||
#
|
||||
# The bot connects to ws://localhost:2333 with the password below (see .env).
|
||||
services:
|
||||
bot:
|
||||
image: git.ksan.dev/ksan/botinator:latest
|
||||
build: .
|
||||
container_name: botinator
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- lavalink
|
||||
# BOT_TOKEN, GUILD_ID, LAVALINK_PASSWORD, ... come from .env.
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- LAVALINK_URI=ws://lavalink:2333
|
||||
# Catchphrases and the AR-meter user lists are supplied at deploy time,
|
||||
# not baked into the image.
|
||||
volumes:
|
||||
- ./catchphrases.txt:/app/catchphrases.txt:ro
|
||||
- ./always100.txt:/app/always100.txt:ro
|
||||
- ./always90.txt:/app/always90.txt:ro
|
||||
|
||||
lavalink:
|
||||
image: ghcr.io/lavalink-devs/lavalink:4
|
||||
container_name: lavalink
|
||||
restart: unless-stopped
|
||||
# A mounted volume at /opt/Lavalink/plugins is created root-owned (the image
|
||||
# has no plugins dir for Docker to copy ownership from), but Lavalink runs as
|
||||
# a non-root user and can't write there. Run as root so it can download the
|
||||
# plugin jars into the volume. The container is internal-only.
|
||||
user: "0:0"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- _JAVA_OPTIONS=-Xmx1G
|
||||
# Lavalink reads this for lavalink.server.password in application.yml.
|
||||
# Keep it equal to LAVALINK_PASSWORD (what the bot uses to connect).
|
||||
- LAVALINK_SERVER_PASSWORD=${LAVALINK_PASSWORD}
|
||||
volumes:
|
||||
- ./lavalink/application.yml:/opt/Lavalink/application.yml:ro
|
||||
- ./lavalink/plugins/:/opt/Lavalink/plugins/
|
||||
# Named volume (managed by Docker) rather than a host bind mount. Docker
|
||||
# initializes it from the image, so it's owned by Lavalink's container user
|
||||
# and the plugin jars download without permission errors. No root-owned (or
|
||||
# UID-mismatched) plugins/ dir in the deploy directory.
|
||||
- lavalink-plugins:/opt/Lavalink/plugins
|
||||
ports:
|
||||
- "2333:2333"
|
||||
|
||||
volumes:
|
||||
lavalink-plugins:
|
||||
|
||||
@@ -11,7 +11,9 @@ lavalink:
|
||||
- dependency: "dev.lavalink.youtube:youtube-plugin:1.18.1"
|
||||
snapshot: false
|
||||
server:
|
||||
password: "youshallnotpass"
|
||||
# Sourced from the LAVALINK_SERVER_PASSWORD env var (set by docker-compose
|
||||
# from LAVALINK_PASSWORD in .env). Required — the server won't start without it.
|
||||
password: "${LAVALINK_SERVER_PASSWORD}"
|
||||
sources:
|
||||
youtube: false # handled by the plugin below, not the core source
|
||||
bandcamp: true
|
||||
|
||||
@@ -34,13 +34,20 @@ public class Main extends ListenerAdapter {
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
String lavalinkPassword = System.getenv("LAVALINK_PASSWORD");
|
||||
if (lavalinkPassword == null || lavalinkPassword.isBlank()) {
|
||||
System.err.println("ERROR: environment variable LAVALINK_PASSWORD is not set. "
|
||||
+ "Set it to your Lavalink server password (see .env) and try again.");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
// Lavalink client must be created before JDA so we can plug in the voice
|
||||
// interceptor. Its node points at a running Lavalink v4 server.
|
||||
LavalinkClient lavalink = new LavalinkClient(Helpers.getUserIdFromToken(token));
|
||||
lavalink.addNode(new NodeOptions.Builder()
|
||||
.setName("main")
|
||||
.setServerUri(getenv("LAVALINK_URI", "ws://localhost:2333"))
|
||||
.setPassword(getenv("LAVALINK_PASSWORD", "youshallnotpass"))
|
||||
.setPassword(lavalinkPassword)
|
||||
.build());
|
||||
|
||||
MusicManager musicManager = new MusicManager(lavalink);
|
||||
|
||||
Reference in New Issue
Block a user