#!/usr/bin/env bash #!/bin/bash # Beschreibung: Minecraft → ntfy Notifier (Join/Leave + optional Up/Down) mit Cron-safe Locking # Autor: Patrick Asmus # Web: https://www.cleveradmin.de # Repository: https://git.techniverse.net/scriptos/minecraft-ntfy-notify # Version: 1.8 # Datum: 19.09.2025 # Änderungen: # - SERVER_NAME im Titel # - GAME_HOST/GAME_PORT im Body # - MESSAGE_EXTRA als zusätzliche Body-Zeile (optional) # - Body-Layout mehrzeilig: Event, Server, Port, [Extra] ##################################################### set -euo pipefail export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # .env laden: bevorzugt ENV_FILE, sonst ./.env if [[ -n "${ENV_FILE:-}" && -f "${ENV_FILE}" ]]; then set -a; . "${ENV_FILE}"; set +a elif [[ -f "${SCRIPT_DIR}/.env" ]]; then set -a; . "${SCRIPT_DIR}/.env"; set +a fi # ===== Konfiguration via ENV ===== # RCON (für Polling) MC_HOST="${MC_HOST:-127.0.0.1}" RCON_PORT="${RCON_PORT:-25575}" RCON_PASSWORD="${RCON_PASSWORD:-changeme}" # Spiel-Adresse (DNS:Port zum Joinen, nur Anzeige) GAME_HOST="${GAME_HOST:-${MC_HOST}}" GAME_PORT="${GAME_PORT:-25565}" POLL_SECONDS="${POLL_SECONDS:-10}" ANNOUNCE_SERVER_UPDOWN="${ANNOUNCE_SERVER_UPDOWN:-true}" # ntfy NTFY_SERVER="${NTFY_SERVER:-}" NTFY_TOPIC="${NTFY_TOPIC:-}" NTFY_TOKEN="${NTFY_TOKEN:-}" NTFY_TITLE_PREFIX="${NTFY_TITLE_PREFIX:-Minecraft}" NTFY_PRIORITY_JOIN="${NTFY_PRIORITY_JOIN:-3}" NTFY_PRIORITY_LEAVE="${NTFY_PRIORITY_LEAVE:-3}" NTFY_PRIORITY_UP="${NTFY_PRIORITY_UP:-4}" NTFY_PRIORITY_DOWN="${NTFY_PRIORITY_DOWN:-5}" NTFY_MARKDOWN="${NTFY_MARKDOWN:-false}" # Anzeigename für den Titel SERVER_NAME="${SERVER_NAME:-}" # Optionale Zusatzzeile im Body MESSAGE_EXTRA="${MESSAGE_EXTRA:-}" # Lock & State je Instanz (Host:Port:Topic) LOCK_KEY="$(printf '%s' "${MC_HOST}_${RCON_PORT}_${NTFY_TOPIC}" | tr -c 'A-Za-z0-9._-' '_')" STATE_DIR="${STATE_DIR:-${SCRIPT_DIR}/state.${LOCK_KEY}}" STATE_PLAYERS="${STATE_DIR}/players.prev" STATE_UP="${STATE_DIR}/server_up.prev" RUN_DIR="${RUN_DIR:-${STATE_DIR}}" LOCK_DIR="${RUN_DIR}/lock.${LOCK_KEY}" PID_FILE="${LOCK_DIR}/pid" RUN_MARK="${RUN_DIR}/running.${LOCK_KEY}" DEBUG="${DEBUG:-false}" # ===== Helpers ===== die() { echo "ERROR: $*" >&2; exit 1; } need_bin() { command -v "$1" >/dev/null 2>&1 || die "Benötigtes Tool fehlt: $1"; } dbg() { [[ "$DEBUG" == "true" ]] && echo "DBG: $*" >&2 || true; } cleanup() { rm -f "$RUN_MARK" "$PID_FILE" 2>/dev/null || true rmdir "$LOCK_DIR" 2>/dev/null || true } acquire_lock() { mkdir -p "$RUN_DIR" if mkdir "$LOCK_DIR" 2>/dev/null; then echo "$$" > "$PID_FILE" trap cleanup EXIT INT TERM else if [[ -f "$PID_FILE" ]]; then oldpid="$(cat "$PID_FILE" 2>/dev/null || true)" if [[ -n "${oldpid:-}" ]] && kill -0 "$oldpid" 2>/dev/null; then exit 0 fi fi rm -rf "$LOCK_DIR" 2>/dev/null || true mkdir "$LOCK_DIR" 2>/dev/null || die "Konnte Lock nicht übernehmen: ${LOCK_DIR}" echo "$$" > "$PID_FILE" trap cleanup EXIT INT TERM fi } build_title() { if [[ -n "$SERVER_NAME" ]]; then printf '%s - %s' "$NTFY_TITLE_PREFIX" "$SERVER_NAME" else printf '%s' "$NTFY_TITLE_PREFIX" fi } # Baut den Body in gewünschtem Layout: # \nServer: \nPort: \n[] build_body() { local event="$1" if [[ -n "$MESSAGE_EXTRA" ]]; then printf '%s\n\nServer: %s\nPort: %s\n%s' \ "$event" "$GAME_HOST" "$GAME_PORT" "$MESSAGE_EXTRA" else printf '%s\nServer: %s\nPort: %s' \ "$event" "$GAME_HOST" "$GAME_PORT" fi } ntfy_notify() { # ntfy_notify "Body" PRIORITY local body="$1" priority="$2" [[ -z "$NTFY_SERVER" || -z "$NTFY_TOPIC" ]] && { echo "ntfy Server/Topic fehlt – skip"; return 1; } local url="${NTFY_SERVER%/}/${NTFY_TOPIC}" local title; title="$(build_title)" local args=(-sS -X POST "$url" -H "Title: ${title}" -H "Priority: ${priority}") [[ "$NTFY_MARKDOWN" == "true" ]] && args+=(-H "Markdown: yes") [[ -n "$NTFY_TOKEN" ]] && args+=(-H "Authorization: Bearer ${NTFY_TOKEN}") curl "${args[@]}" --data-raw "$body" >/dev/null 2>&1 || return 1 return 0 } ensure_state() { mkdir -p "$STATE_DIR" touch "$STATE_PLAYERS" [[ -f "$STATE_UP" ]] || echo "unknown" >"$STATE_UP" } # ANSI- & MC-Farbcodes entfernen sanitize() { sed -E $'s/\x1B\\[[0-9;]*[A-Za-z]//g' \ | sed -E 's/§[0-9A-FK-ORa-fk-or]//g' \ | tr -d '\r' \ | sed -E 's/[\x00-\x1F\x7F]//g' } get_players() { local out if ! out=$(mcrcon -H "$MC_HOST" -P "$RCON_PORT" -p "$RCON_PASSWORD" "list" 2>/dev/null); then return 1 fi dbg "RAW: $out" out="$(printf '%s' "$out" | sanitize)" if ! grep -q "players online:" <<<"$out"; then return 0 fi local names="${out#*:}" names="$(echo "$names" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" if [[ -z "$names" ]]; then return 0 fi printf '%s\n' "$names" \ | tr ',' '\n' \ | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \ | sed '/^$/d' \ | LC_ALL=C sort -u return 0 } # ===== Checks & Lock ===== need_bin curl need_bin mcrcon [[ -n "${NTFY_SERVER}" && -n "${NTFY_TOPIC}" ]] || die "NTFY_SERVER/NTFY_TOPIC nicht gesetzt" ensure_state acquire_lock echo "Starte Polling RCON ${MC_HOST}:${RCON_PORT} -> ntfy ${NTFY_SERVER}/${NTFY_TOPIC} (PID $$)" prev_up="$(cat "$STATE_UP")" # ===== Main Loop ===== while :; do date -Iseconds > "$RUN_MARK"; echo "$$" >> "$RUN_MARK" tmp_players="$(mktemp)" if get_players >"$tmp_players"; then server_up="true" else server_up="false" : >"$tmp_players" fi if [[ "$ANNOUNCE_SERVER_UPDOWN" == "true" && "$prev_up" != "unknown" && "$server_up" != "$prev_up" ]]; then if [[ "$server_up" == "true" ]]; then ntfy_notify "$(build_body "Server ist wieder erreichbar.")" "$NTFY_PRIORITY_UP" || true else ntfy_notify "$(build_body "Server ist nicht erreichbar.")" "$NTFY_PRIORITY_DOWN" || true fi fi if [[ "$server_up" == "true" ]]; then LC_ALL=C sort -u "$STATE_PLAYERS" -o "$STATE_PLAYERS" joined="$(comm -13 "$STATE_PLAYERS" "$tmp_players" || true)" left="$(comm -23 "$STATE_PLAYERS" "$tmp_players" || true)" if [[ -n "$joined" ]]; then while IFS= read -r name; do [[ -z "$name" ]] && continue ntfy_notify "$(build_body "Player \"${name}\" hat den Server betreten.")" "$NTFY_PRIORITY_JOIN" || true done <<<"$joined" fi if [[ -n "$left" ]]; then while IFS= read -r name; do [[ -z "$name" ]] && continue ntfy_notify "$(build_body "Player \"${name}\" hat den Server verlassen.")" "$NTFY_PRIORITY_LEAVE" || true done <<<"$left" fi mv "$tmp_players" "$STATE_PLAYERS" else rm -f "$tmp_players" fi echo -n "$server_up" >"$STATE_UP" prev_up="$server_up" sleep "$POLL_SECONDS" done