diff --git a/.env b/.env new file mode 100644 index 0000000..ee4df80 --- /dev/null +++ b/.env @@ -0,0 +1,27 @@ +# Copy this file to .env and fill in your values +# Discord bot token and channel +DISCORD_TOKEN=MTA3MTEwMzcwMDAyMzY0ODMyMA.GIlFkn.o10LKcpjlaLDvVsgF_WKZhz5ykPYf1KEds_g3I +DISCORD_CHANNEL_ID=1247178901004877916 +# Optional if using webhooks +WEBHOOK_URL= + +# Display name and avatar for server-origin messages when using webhooks +SERVER_NAME=TFMC +# Optional image URL to use as the avatar for server messages +# SERVER_IMAGE=https://example.com/server-icon.png + +# If logs are mounted, keep true; set to false to use remote webhook mode +IS_LOCAL_FILE=true + +# Path on host to the Minecraft server logs directory containing latest.log +MC_LOGS_DIR=./data/logs + +# Optionally override the log path inside the container +# LOCAL_FILE_PATH_OVERRIDE=/minecraft/logs/latest.log + +# RCON connection to send Discord -> Minecraft messages +# Ensure server.properties has enable-rcon=true and set password/port +# Set IP to the Minecraft server host or container name on the Docker network +MINECRAFT_SERVER_RCON_IP=mc +MINECRAFT_SERVER_RCON_PORT=25575 +MINECRAFT_SERVER_RCON_PASSWORD=509dbd71b996df2bd575d43b diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..40c894a --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +# Copy this file to .env and fill in your values +# Discord bot token and channel +DISCORD_TOKEN= +DISCORD_CHANNEL_ID= +# Optional if using webhooks +WEBHOOK_URL= + +# Display name and avatar for server-origin messages when using webhooks +SERVER_NAME=Shulker +# Optional image URL to use as the avatar for server messages +# SERVER_IMAGE=https://example.com/server-icon.png + +# If logs are mounted, keep true; set to false to use remote webhook mode +IS_LOCAL_FILE=true + +# Path on host to the Minecraft server logs directory containing latest.log +MC_LOGS_DIR=./data/logs + +# Optionally override the log path inside the container +# LOCAL_FILE_PATH_OVERRIDE=/minecraft/logs/latest.log + +# RCON connection to send Discord -> Minecraft messages +# Ensure server.properties has enable-rcon=true and set password/port +# Set IP to the Minecraft server host or container name on the Docker network +MINECRAFT_SERVER_RCON_IP=127.0.0.1 +MINECRAFT_SERVER_RCON_PORT=25575 +MINECRAFT_SERVER_RCON_PASSWORD= diff --git a/README.md b/README.md index a1e83dd..75d7838 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,55 @@ Website for TFMC (teknisk fysiks minecraft server) - Maybe use integration with itgz, and get variables from those servers? via for example docker sock? or would that be to insecure? [] Maybe integrate chat with discord via rcon? - Base on existing project? This one exist and could maybe be useful? https://github.com/destruc7i0n/shulker + +## Shulker Discord-Minecraft Bridge (Dockerized) + +This repo includes [Shulker](https://github.com/destruc7i0n/shulker) as a Git submodule under `external/shulker`, plus a Docker setup so you can run it and mount your Minecraft logs as a volume. + +### Prereqs +- Docker and Docker Compose installed +- A Discord bot token and (optionally) a Discord channel webhook +- Your Minecraft server configured with rcon and accessible logs + +### First-time setup +1. Initialize and update submodules: + - git submodule update --init --recursive +2. Configure environment variables (create a `.env` in repo root or pass via CLI): + - DISCORD_TOKEN=... (required) + - DISCORD_CHANNEL_ID=... (required if not using webhooks only) + - WEBHOOK_URL=... (if using webhooks) + - MC_LOGS_DIR=/absolute/path/to/your/minecraft/logs (defaults to `./Worlds/tfmc23-24/logs`) + - IS_LOCAL_FILE=true to tail logs directly (recommended when logs are mounted) + +### Run +Start the service (from repo root): + +``` +docker compose up -d --build shulker +``` + +On first run, `/data/config.json` will be created from `config.example.json`. The container will symlink it to `/app/config.json` and set `IS_LOCAL_FILE` and `LOCAL_FILE_PATH` to `/minecraft/logs/latest.log` by default. You can edit `config.json` by entering the container or by stopping the container and editing the file in the named volume. + +Expose the Shulker webhook on port 8000 if you want to send remote hooks instead of tailing logs. For remote setup, set `IS_LOCAL_FILE=false` and follow the command in Shulker's README to pipe log lines to the webhook. + +### Volumes +- `shulker_data` named volume stores `config.json`. +- `${MC_LOGS_DIR}` (host) is mounted read-only at `/minecraft/logs` (container). Ensure this path contains `latest.log`. + +### Updating Shulker +Shulker is a submodule: + +``` +git submodule update --remote external/shulker +``` + +Rebuild the image after updating: + +``` +docker compose build shulker && docker compose up -d shulker +``` + +### Notes +- Node 16+ is required; the Docker image uses Node 18 Alpine. +- If your server is modded, you may need to adjust `REGEX_SERVER_PREFIX` per Shulker’s FAQ. +- For RCON features, set `MINECRAFT_SERVER_RCON_*` in `config.json` and ensure your server has rcon enabled. diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..0fa8396 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory +* + +# But keep this file so the directory exists in the repo +!.gitignore diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..afbfdb3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +version: "3.8" + +services: + shulker: + build: + context: . + dockerfile: docker/shulker/Dockerfile + container_name: shulker + restart: unless-stopped + environment: + # Provide these via environment or .env file + - DISCORD_TOKEN=${DISCORD_TOKEN} + - DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ID} + - WEBHOOK_URL=${WEBHOOK_URL} + - SERVER_NAME=${SERVER_NAME:-Shulker} + - SERVER_IMAGE=${SERVER_IMAGE} + - DISCORD_MESSAGE_TEMPLATE=${DISCORD_MESSAGE_TEMPLATE} + - DEBUG=${DEBUG} + - ALLOW_USER_MENTIONS=${ALLOW_USER_MENTIONS} + - ALLOW_HERE_EVERYONE_MENTIONS=${ALLOW_HERE_EVERYONE_MENTIONS} + - MINECRAFT_SERVER_RCON_IP=${MINECRAFT_SERVER_RCON_IP} + - MINECRAFT_SERVER_RCON_PORT=${MINECRAFT_SERVER_RCON_PORT} + - MINECRAFT_SERVER_RCON_PASSWORD=${MINECRAFT_SERVER_RCON_PASSWORD} + - MINECRAFT_TELLRAW_DOESNT_EXIST=${MINECRAFT_TELLRAW_DOESNT_EXIST} + - MINECRAFT_TELLRAW_TEMPLATE=${MINECRAFT_TELLRAW_TEMPLATE} + - MINECRAFT_TELLRAW_DOESNT_EXIST_SAY_TEMPLATE=${MINECRAFT_TELLRAW_DOESNT_EXIST_SAY_TEMPLATE} + # Set to false if using remote hook mode + - IS_LOCAL_FILE=${IS_LOCAL_FILE:-true} + # Optionally override the log path inside the container + - LOCAL_FILE_PATH_OVERRIDE=${LOCAL_FILE_PATH_OVERRIDE} + volumes: + # Persist config.json in a named volume + - shulker_data:/data + # Map your host/server Minecraft logs into the container + - ${MC_LOGS_DIR:-./data/logs}:/minecraft/logs:ro + ports: + - "8000:8000" + + mc: + image: itzg/minecraft-server + tty: true + stdin_open: true + ports: + - "25565:25565" + environment: + EULA: "TRUE" + ENABLE_RCON: "true" + RCON_PASSWORD: ${MINECRAFT_SERVER_RCON_PASSWORD} + RCON_PORT: ${MINECRAFT_SERVER_RCON_PORT} + # Ensure env values are written into server.properties on each start + OVERRIDE_SERVER_PROPERTIES: "true" + volumes: + # attach the relative directory 'data' to the container's /data path + - ./data:/data + +volumes: + shulker_data: + driver: local diff --git a/docker/shulker/Dockerfile b/docker/shulker/Dockerfile new file mode 100644 index 0000000..9a285fb --- /dev/null +++ b/docker/shulker/Dockerfile @@ -0,0 +1,49 @@ +# Build Shulker without modifying the submodule; run as non-root; persist config and read logs via volumes +FROM node:22-alpine AS build + +WORKDIR /app + +# Enable corepack so yarn is available (Node 18+) +RUN corepack enable + +# Copy only dependency manifests first for better layer caching +COPY external/shulker/package.json external/shulker/yarn.lock ./external/shulker/ + +WORKDIR /app/external/shulker + +# Install dependencies +RUN yarn install --frozen-lockfile || yarn install + +# Copy source and build +COPY external/shulker/ /app/external/shulker/ +RUN yarn build + +FROM node:22-alpine + +WORKDIR /app + +# Copy built app from builder +COPY --from=build /app/external/shulker /app + +# Prepare runtime directories and non-root user +RUN adduser -D -h /app -u 10001 shulker \ + && mkdir -p /data /minecraft/logs \ + && chown -R shulker:shulker /app /data /minecraft + +# Copy entrypoint +COPY docker/shulker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Install helpful runtime tools +RUN apk add --no-cache jq + +USER shulker + +# Shulker default port +EXPOSE 8000 + +# Volumes: /data holds config.json, /minecraft/logs maps to the server logs +VOLUME ["/data", "/minecraft/logs"] + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["node", "build/index.js"] diff --git a/docker/shulker/entrypoint.sh b/docker/shulker/entrypoint.sh new file mode 100644 index 0000000..48516d0 --- /dev/null +++ b/docker/shulker/entrypoint.sh @@ -0,0 +1,90 @@ +#!/bin/sh +set -e + +# If no config exists in /data, seed it from the example and apply env overrides if provided +if [ ! -f /data/config.json ]; then + echo "Seeding /data/config.json from example" + cp /app/config.example.json /data/config.json +fi + +# Ensure LOCAL_FILE_PATH points to the mounted logs unless explicitly overridden via env +if [ -z "${LOCAL_FILE_PATH_OVERRIDE}" ]; then + LOCAL_FILE_PATH="/minecraft/logs/latest.log" +else + LOCAL_FILE_PATH="${LOCAL_FILE_PATH_OVERRIDE}" +fi + +# Apply a few common ENV overrides into config.json if provided +json_set() { + KEY="$1"; VALUE="$2"; FILE="$3" + # Use jq if available for robust edits; otherwise sed fallback + if command -v jq >/dev/null 2>&1; then + tmp=$(mktemp) + jq --arg v "$VALUE" ".[$KEY]=$VALUE" "$FILE" > "$tmp" && mv "$tmp" "$FILE" || true + else + # naive sed replace; expects simple string or number values already present + sed -i "s#\(\"$KEY\"\): .*#\\\"$KEY\\\": $VALUE,#" "$FILE" || true + fi +} + +# Build a small jq script to apply overrides +if command -v jq >/dev/null 2>&1; then + jq \ + --argfile cfg /data/config.json \ + '.' >/dev/null 2>&1 || true +fi + +# When env vars are set, write them. Values must be valid JSON literals (strings quoted). +[ -n "${DISCORD_TOKEN}" ] && jq ".DISCORD_TOKEN=\"${DISCORD_TOKEN}\"" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json || true +[ -n "${DISCORD_CHANNEL_ID}" ] && jq ".DISCORD_CHANNEL_ID=\"${DISCORD_CHANNEL_ID}\"" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json || true +[ -n "${WEBHOOK_URL}" ] && jq ".WEBHOOK_URL=\"${WEBHOOK_URL}\"" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json || true +[ -n "${DISCORD_MESSAGE_TEMPLATE}" ] && jq ".DISCORD_MESSAGE_TEMPLATE=\"${DISCORD_MESSAGE_TEMPLATE}\"" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json || true +[ -n "${DEBUG}" ] && jq ".DEBUG=${DEBUG}" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json || true + +# Default behavior: if no WEBHOOK_URL, disable webhooks; otherwise enable them. +# If USE_WEBHOOKS is explicitly set via env, it overrides this default. +if [ -z "${USE_WEBHOOKS}" ]; then + if jq -e '.WEBHOOK_URL and (.WEBHOOK_URL | length) > 0' /data/config.json >/dev/null 2>&1; then + jq ".USE_WEBHOOKS=true" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json + else + jq ".USE_WEBHOOKS=false" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json + fi +else + case "${USE_WEBHOOKS}" in + [Tt][Rr][Uu][Ee]|1|yes|on) jq ".USE_WEBHOOKS=true" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json ;; + [Ff][Aa][Ll][Ss][Ee]|0|no|off) jq ".USE_WEBHOOKS=false" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json ;; + esac +fi + +# Customize server identity shown in Discord when posting server messages via webhook +[ -n "${SERVER_NAME}" ] && jq ".SERVER_NAME=\"${SERVER_NAME}\"" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json || true +[ -n "${SERVER_IMAGE}" ] && jq ".SERVER_IMAGE=\"${SERVER_IMAGE}\"" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json || true + +# RCON configuration overrides +[ -n "${MINECRAFT_SERVER_RCON_IP}" ] && jq ".MINECRAFT_SERVER_RCON_IP=\"${MINECRAFT_SERVER_RCON_IP}\"" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json || true +if [ -n "${MINECRAFT_SERVER_RCON_PORT}" ]; then + case "${MINECRAFT_SERVER_RCON_PORT}" in + ''|*[!0-9]*) jq ".MINECRAFT_SERVER_RCON_PORT=\"${MINECRAFT_SERVER_RCON_PORT}\"" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json ;; + *) jq ".MINECRAFT_SERVER_RCON_PORT=${MINECRAFT_SERVER_RCON_PORT}" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json ;; + esac +fi +[ -n "${MINECRAFT_SERVER_RCON_PASSWORD}" ] && jq ".MINECRAFT_SERVER_RCON_PASSWORD=\"${MINECRAFT_SERVER_RCON_PASSWORD}\"" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json || true + +# Minecraft output formatting overrides +if [ -n "${MINECRAFT_TELLRAW_DOESNT_EXIST}" ]; then + case "${MINECRAFT_TELLRAW_DOESNT_EXIST}" in + [Tt][Rr][Uu][Ee]|1|yes|on) jq ".MINECRAFT_TELLRAW_DOESNT_EXIST=true" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json ;; + [Ff][Aa][Ll][Ss][Ee]|0|no|off) jq ".MINECRAFT_TELLRAW_DOESNT_EXIST=false" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json ;; + esac +fi +[ -n "${MINECRAFT_TELLRAW_TEMPLATE}" ] && jq ".MINECRAFT_TELLRAW_TEMPLATE=\"${MINECRAFT_TELLRAW_TEMPLATE}\"" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json || true +[ -n "${MINECRAFT_TELLRAW_DOESNT_EXIST_SAY_TEMPLATE}" ] && jq ".MINECRAFT_TELLRAW_DOESNT_EXIST_SAY_TEMPLATE=\"${MINECRAFT_TELLRAW_DOESNT_EXIST_SAY_TEMPLATE}\"" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json || true + +# Always set IS_LOCAL_FILE true and path to mounted logs unless overridden explicitly via env IS_LOCAL_FILE +if [ -z "${IS_LOCAL_FILE}" ]; then IS_LOCAL_FILE=true; fi +jq ".IS_LOCAL_FILE=${IS_LOCAL_FILE} | .LOCAL_FILE_PATH=\"${LOCAL_FILE_PATH}\"" /data/config.json > /data/config.json.tmp && mv /data/config.json.tmp /data/config.json + +# Link config into app directory where shulker expects it +ln -sf /data/config.json /app/config.json + +exec "$@" diff --git a/millerman03 b/millerman03 deleted file mode 100644 index 543ae00..0000000 --- a/millerman03 +++ /dev/null @@ -1,4 +0,0 @@ -{ - "id" : "edc64ca33ed24de1852b11051edc82e5", - "name" : "Millerman03" -} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29