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 ###
|
### Local secrets ###
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
# ...but keep the committed template.
|
||||||
|
!.env.example
|
||||||
*.local
|
*.local
|
||||||
secrets.properties
|
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`.
|
comes from the `youtube-plugin`, configured in `lavalink/application.yml`.
|
||||||
|
|
||||||
- Bot: Java + Gradle (JDA 6, `lavalink-client`)
|
- Bot: Java + Gradle (JDA 6, `lavalink-client`)
|
||||||
- Music server: Lavalink v4 via `docker-compose.yml`
|
- Music server: Lavalink v4 via `docker-compose.yaml`
|
||||||
|
|
||||||
## Requirements
|
## 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)
|
- Docker + Docker Compose (to run the Lavalink server)
|
||||||
- A Discord bot token
|
- A Discord bot token
|
||||||
|
|
||||||
@@ -47,8 +47,8 @@ cp .env.example .env
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `BOT_TOKEN` | ✅ | Discord bot token (Developer Portal → your app → Bot) |
|
| `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) |
|
| `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_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` | optional | Defaults to `youshallnotpass` (matches `lavalink/application.yml`) |
|
| `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.
|
`.env` is gitignored, so your token stays local.
|
||||||
|
|
||||||
@@ -58,15 +58,20 @@ cp .env.example .env
|
|||||||
So load it into your shell first.
|
So load it into your shell first.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Start the Lavalink server (first run downloads the YouTube plugin; wait for "ready")
|
# 1. Start ONLY the Lavalink server — not the bot container — so it doesn't clash
|
||||||
docker compose up -d
|
# with the bot you're running from source. (First run downloads the YouTube
|
||||||
docker compose logs -f # wait for "Lavalink is ready to accept connections", then Ctrl+C
|
# 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
|
# 2. Load your .env and run the bot
|
||||||
set -a; source .env; set +a
|
set -a; source .env; set +a
|
||||||
./gradlew run
|
./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
|
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>`.
|
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
|
- [ ] Invite the bot to your server with the `bot` and `applications.commands` scopes
|
||||||
(and the **Connect** + **Speak** voice permissions)
|
(and the **Connect** + **Speak** voice permissions)
|
||||||
- [ ] Enable Developer Mode in Discord and copy your server's **Guild ID**
|
- [ ] Enable Developer Mode in Discord and copy your server's **Guild ID**
|
||||||
- [ ] `cp .env.example .env` and fill in `BOT_TOKEN` (and `GUILD_ID`)
|
- [ ] `cp .env.example .env` and fill in `BOT_TOKEN`, `LAVALINK_PASSWORD` (and `GUILD_ID`)
|
||||||
- [ ] Start Lavalink: `docker compose up -d` and wait until the logs say it's ready
|
- [ ] 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`
|
- [ ] Load env vars: `set -a; source .env; set +a`
|
||||||
- [ ] Run the bot: `./gradlew run`
|
- [ ] Run the bot: `./gradlew run`
|
||||||
- [ ] In Discord, run `/hello` to confirm it responds, then `/play` from a voice channel
|
- [ ] 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
|
## Notes
|
||||||
|
|
||||||
- Queue state is kept in memory, so it resets if the bot restarts.
|
- 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.
|
# Runs the bot together with a Lavalink v4 server for music playback.
|
||||||
# docker compose up -d # start in background
|
# docker compose up -d --build # build the bot image locally and start
|
||||||
# docker compose logs -f # watch logs
|
|
||||||
# docker compose down # stop
|
|
||||||
#
|
#
|
||||||
# The bot connects to ws://localhost:2333 with the password below (see .env).
|
|
||||||
services:
|
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:
|
lavalink:
|
||||||
image: ghcr.io/lavalink-devs/lavalink:4
|
image: ghcr.io/lavalink-devs/lavalink:4
|
||||||
container_name: lavalink
|
container_name: lavalink
|
||||||
restart: unless-stopped
|
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:
|
environment:
|
||||||
- _JAVA_OPTIONS=-Xmx1G
|
- _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:
|
volumes:
|
||||||
- ./lavalink/application.yml:/opt/Lavalink/application.yml:ro
|
- ./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:
|
ports:
|
||||||
- "2333:2333"
|
- "2333:2333"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
lavalink-plugins:
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ lavalink:
|
|||||||
- dependency: "dev.lavalink.youtube:youtube-plugin:1.18.1"
|
- dependency: "dev.lavalink.youtube:youtube-plugin:1.18.1"
|
||||||
snapshot: false
|
snapshot: false
|
||||||
server:
|
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:
|
sources:
|
||||||
youtube: false # handled by the plugin below, not the core source
|
youtube: false # handled by the plugin below, not the core source
|
||||||
bandcamp: true
|
bandcamp: true
|
||||||
|
|||||||
@@ -34,13 +34,20 @@ public class Main extends ListenerAdapter {
|
|||||||
System.exit(1);
|
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
|
// Lavalink client must be created before JDA so we can plug in the voice
|
||||||
// interceptor. Its node points at a running Lavalink v4 server.
|
// interceptor. Its node points at a running Lavalink v4 server.
|
||||||
LavalinkClient lavalink = new LavalinkClient(Helpers.getUserIdFromToken(token));
|
LavalinkClient lavalink = new LavalinkClient(Helpers.getUserIdFromToken(token));
|
||||||
lavalink.addNode(new NodeOptions.Builder()
|
lavalink.addNode(new NodeOptions.Builder()
|
||||||
.setName("main")
|
.setName("main")
|
||||||
.setServerUri(getenv("LAVALINK_URI", "ws://localhost:2333"))
|
.setServerUri(getenv("LAVALINK_URI", "ws://localhost:2333"))
|
||||||
.setPassword(getenv("LAVALINK_PASSWORD", "youshallnotpass"))
|
.setPassword(lavalinkPassword)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
MusicManager musicManager = new MusicManager(lavalink);
|
MusicManager musicManager = new MusicManager(lavalink);
|
||||||
|
|||||||
Reference in New Issue
Block a user