Перейти к содержимому

Minecraft Discord Webhook: уведомления о входе, выходе и смерти

Уведомления Minecraft-сервера в Discord через webhook: вход, выход и смерти — плагин для Paper, bash-скрипт по логу для vanilla, cron и Pterodactyl. Без бота.

6 мин чтения Доступно также на English
На этой странице

Комьюнити Minecraft-сервера и так сидит в Discord, поэтому «кто зашёл» и «кого опять убил крипер» логично видеть в канале, а не только в консоли. Вариантов два: бот-мост вроде DiscordSRV или обычный webhook, в который сервер сам отправляет события. Ниже — честное сравнение и три рабочих реализации на вебхуке: маленький плагин для Paper, bash-скрипт, который читает latest.log на vanilla и модовых серверах, и связка cron/Pterodactyl.

Бот или webhook: что выбрать

DiscordSRV — самое популярное полноценное решение для серверов на Bukkit (Spigot, Paper, Purpur). Внутри плагина работает Discord-бот, поэтому он умеет то, что вебхуку недоступно в принципе: двусторонний чат между игрой и каналом, канал-консоль, куда можно писать команды, привязку аккаунтов и готовые сообщения о входе, выходе, смерти и достижениях. Плата — настройка: создать приложение в Discord, получить токен бота, пригласить бота с нужными правами, прописать токен и ID каналов в конфиг и хранить токен в секрете. Нужен именно мост чата — ставьте DiscordSRV и дальше можно не читать.

Webhook подходит, когда нужны только односторонние уведомления: вход, выход, смерть, достижения, «сервер запущен», «бэкап готов». Ни бота, ни токенов, кроме самого URL вебхука, и работает из любого места, откуда можно сделать HTTP-запрос: из плагина, shell-скрипта, cron-задачи, панели хостинга. Если разница всё ещё не очевидна, читайте бот против вебхука.

Всё, что ниже, — про вебхуки.

Создаём webhook

В Discord откройте нужный канал → Редактировать канал → Интеграции → Вебхуки → Новый вебхук. Задайте имя, выберите канал, скопируйте URL. Он выглядит как https://discord.com/api/webhooks/{id}/{token}, и токен — секрет: любой, у кого есть URL, может писать в ваш канал. Храните его в конфиге с ограниченными правами или в переменной окружения, но не в публичном репозитории плагина и не в сборке, которую раздаёте игрокам. Утёк — удалите вебхук и создайте новый. Скриншоты есть в руководстве по настройке.

Вариант 1: плагин для Paper — вход, выход и смерть

Готовые webhook-плагины есть на SpigotMC и Hangar, но вся задача укладывается в сотню строк, а свой код — это отсутствие чужого кода на сервере. Плагин ниже работает на Spigot и Paper 1.20+ (для 1.20.5 и новее нужна Java 21). Использует Gson, который обе платформы поставляют в комплекте, и встроенный в Java HttpClient.

plugin.yml и config.yml

# src/main/resources/plugin.yml
name: McWebhook
version: 1.0.0
main: com.example.mcwebhook.McWebhook
api-version: '1.20'
# src/main/resources/config.yml
webhook-url: 'https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN'
username: 'Minecraft'

Класс плагина

package com.example.mcwebhook;

import com.google.gson.Gson;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.java.JavaPlugin;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;

public final class McWebhook extends JavaPlugin implements Listener {
    private final HttpClient http = HttpClient.newHttpClient();
    private final Gson gson = new Gson();
    private String webhookUrl;

    @Override
    public void onEnable() {
        saveDefaultConfig();
        webhookUrl = getConfig().getString("webhook-url", "");
        if (webhookUrl.isBlank()) {
            getLogger().severe("webhook-url is empty in config.yml, disabling");
            getServer().getPluginManager().disablePlugin(this);
            return;
        }
        getServer().getPluginManager().registerEvents(this, this);
    }

    @EventHandler
    public void onJoin(PlayerJoinEvent e) {
        String name = e.getPlayer().getName();
        send(name, "**" + name + "** зашёл на сервер", 5763719);
    }

    @EventHandler
    public void onQuit(PlayerQuitEvent e) {
        String name = e.getPlayer().getName();
        send(name, "**" + name + "** вышел с сервера", 9807270);
    }

    @EventHandler
    public void onDeath(PlayerDeathEvent e) {
        String name = e.getEntity().getName();
        String msg = e.getDeathMessage(); // на Paper deprecated, но работает
        send(name, "💀 " + (msg == null ? name + " погиб" : msg), 15548997);
    }

    private void send(String player, String description, int color) {
        Map<String, Object> payload = Map.of(
            "username", getConfig().getString("username", "Minecraft"),
            "embeds", List.of(Map.of(
                "description", description,
                "color", color,
                "thumbnail", Map.of("url", "https://mc-heads.net/avatar/" + player + "/64"),
                "footer", Map.of("text", Bukkit.getOnlinePlayers().size() + "/" + Bukkit.getMaxPlayers() + " онлайн")
            ))
        );
        HttpRequest req = HttpRequest.newBuilder(URI.create(webhookUrl))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
            .build();
        // sendAsync не блокирует главный поток сервера
        http.sendAsync(req, HttpResponse.BodyHandlers.ofString())
            .thenAccept(r -> {
                if (r.statusCode() >= 400) {
                    getLogger().warning("Discord returned " + r.statusCode() + ": " + r.body());
                }
            });
    }
}

Три важные детали. JSON собирает Gson, поэтому кавычки в сообщении о смерти не сломают тело запроса. sendAsync уводит запрос с главного потока: синхронный вызов подвешивал бы каждый тик, пока Discord не ответит. Миниатюра берётся с публичного сервиса рендера голов, потому что thumbnail.url обязан быть публичной HTTPS-картинкой. Нюанс: во время PlayerQuitEvent уходящий игрок ещё числится онлайн, так что в футере там на единицу больше — вычтите один, если мешает.

Сборка и установка

Подключите io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT из репозитория https://repo.papermc.io/repository/maven-public/ как provided-зависимость в Maven (compileOnly в Gradle), выполните mvn package, положите jar в plugins/, запустите сервер один раз, чтобы появился plugins/McWebhook/config.yml, вставьте URL, перезапустите. Зайдите на сервер — embed должен прийти в течение секунды.

Вариант 2: читаем latest.log на vanilla, Fabric и Forge

Vanilla-сервер плагины не грузит, а модовым нужен отдельный мод под каждый загрузчик. Но любой сервер Java Edition пишет в logs/latest.log одни и те же строки вроде [12:34:56] [Server thread/INFO]: Steve joined the game или Steve was slain by Zombie. Paper сокращает префикс до [12:34:56 INFO]:, а Forge и NeoForge вставляют после него имя логгера ([Server thread/INFO] [net.minecraft.server.MinecraftServer/]:), поэтому скрипт ниже отрезает всё до INFO], а затем до первого : , и работает на всех. Нужны curl и jq.

Скрипт

#!/usr/bin/env bash
# /opt/minecraft/mc-discord.sh
set -uo pipefail

WEBHOOK_URL="${DISCORD_WEBHOOK_URL:?задайте DISCORD_WEBHOOK_URL}"
LOG="${MC_LOG:-/opt/minecraft/logs/latest.log}"

send() {  # $1 = описание, $2 = цвет в десятичном виде
  jq -cn --arg d "$1" --argjson c "$2" \
    '{username: "Minecraft", embeds: [{description: $d, color: $c, timestamp: (now | todate)}]}' \
  | curl -sS -o /dev/null --retry 3 --retry-delay 2 \
      -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
}

DEATH='^[A-Za-z0-9_]{3,16} (was |died|drowned|blew up|burned|fell|hit the ground|tried to swim|starved|suffocated|withered|froze|went up in flames|walked into|discovered|experienced)'

# -n 0: без истории; -F: переживает ротацию лога
tail -n 0 -F "$LOG" | while IFS= read -r line; do
  msg="${line#*INFO]}"
  [[ "$msg" == "$line" ]] && continue   # не INFO-строка
  msg="${msg#*: }"                      # убираем ": " или " [логгер]: " у Forge
  [[ "$msg" == \<* ]] && continue       # "<Steve> ..." — чат игроков, ему нельзя верить
  case "$msg" in
    *" joined the game")            send "**${msg% joined the game}** зашёл на сервер" 5763719 ;;
    *" left the game")              send "**${msg% left the game}** вышел с сервера" 9807270 ;;
    *" has made the advancement "*) send "🏆 $msg" 16776960 ;;
    *) [[ "$msg" =~ $DEATH ]] && send "💀 $msg" 15548997 ;;
  esac
done

Именно tail -F (заглавная F) переживает ротацию лога Minecraft: при каждом старте сервера latest.log сжимается в gzip и создаётся заново, а -F заново открывает файл по имени. -n 0 гарантирует, что перезапуск скрипта не выльет в канал старые входы. --retry заставляет curl (7.71 и новее) считать 429 временной ошибкой и ждать столько, сколько Discord указал в заголовке Retry-After.

Запускаем как сервис

# /etc/systemd/system/mc-discord.service
[Unit]
Description=Minecraft log to Discord
After=minecraft.service

[Service]
User=minecraft
EnvironmentFile=/etc/minecraft/discord.env
ExecStart=/opt/minecraft/mc-discord.sh
Restart=always

[Install]
WantedBy=multi-user.target

В /etc/minecraft/discord.env лежит DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... с правами chmod 600. Дальше systemctl enable --now mc-discord.

Pterodactyl и cron

Pterodactyl

В Pterodactyl сервер живёт в Docker-контейнере под управлением Wings, а его файлы лежат на ноде в /var/lib/pterodactyl/volumes/<uuid-сервера>/. UUID виден на странице Settings сервера в панели. Направьте тот же скрипт на этот путь и запустите его как systemd-юнит на ноде:

MC_LOG=/var/lib/pterodactyl/volumes/3f2a1c7e-0000-0000-0000-000000000000/logs/latest.log

Если вы арендуете сервер на чужой панели Pterodactyl и доступа к ноде нет, контейнер — неудачное место для tail-цикла; ставьте плагин или его аналог для Fabric/Forge.

Cron: статус раз в час через RCON

Включите RCON в server.properties (enable-rcon=true, rcon.port=25575, rcon.password=...) и поставьте mcrcon. Ванильная команда list отвечает строкой There are 3 of a max of 20 players online: Steve, Alex, Notch — её можно отправлять как есть:

#!/usr/bin/env bash
# /opt/minecraft/mc-status.sh
set -uo pipefail
WEBHOOK_URL="${DISCORD_WEBHOOK_URL:?}"

if out=$(mcrcon -H 127.0.0.1 -P 25575 -p "$RCON_PASSWORD" list 2>/dev/null); then
  title="Сервер онлайн"; color=5763719
else
  title="Сервер офлайн"; color=15548997; out="RCON не ответил. Проверьте процесс."
fi

jq -cn --arg t "$title" --arg d "$out" --argjson c "$color" \
  '{embeds: [{title: $t, description: $d, color: $c, timestamp: (now | todate)}]}' \
| curl -sS -o /dev/null -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
# crontab -e
0 * * * * set -a; . /etc/minecraft/discord.env; /opt/minecraft/mc-status.sh

RCON_PASSWORD=... положите в тот же env-файл; именно set -a экспортирует его переменные в скрипт, а просто . оставил бы их локальными для оболочки. Если интересен только даунтайм — храните последнее состояние в файле и отправляйте сообщение лишь при его смене; этот приём разобран в статье про алерты аптайма.

Пинг роли, когда сервер упал

Ветка «офлайн» — та, ради которой уведомления и заводят, так что пусть она пингует. Задавайте allowed_mentions явно: уведомление получат только перечисленные там роли, а с { "parse": [] } тот же текст отобразится без пинга. Если поле не передать, Discord пингует всё, что найдёт в content:

{
  "content": "<@&123456789012345678> Minecraft-сервер упал",
  "allowed_mentions": { "roles": ["123456789012345678"] },
  "embeds": [{ "title": "Сервер офлайн", "color": 15548997 }]
}

@everyone в почасовом выводе cron делать не надо. Остальное про allowed_mentions — в статье об упоминаниях. Подобрать раскладку, цвета и миниатюру до того, как зашивать их в скрипт, удобно в конструкторе Discord Webhook: живой предпросмотр и экспорт JSON, который потом отдаёте jq или Gson.

Частые ошибки

  • Ничего не приходит и ошибок нет. Скрипт читает не тот файл или формат строк отличается. Выполните tail -F logs/latest.log | grep 'INFO]' и убедитесь, что строка входа появляется, пока игрок заходит.
  • 400 Bad Request. Собранный вручную JSON сломался на кавычке, обратном слеше или переносе строки в сообщении о смерти. Собирайте тело через jq или Gson, как выше; в ответе Discord назовёт проблемное поле.
  • 404 с {"message": "Unknown Webhook", "code": 10015}. Вебхук удалён или URL скопирован с потерянным или лишним символом. Пересоздайте его в Настройки сервера → Интеграции → Вебхуки.
  • 429 Too Many Requests после рестарта. Когда все переподключаются одновременно, вы шлёте пачку сообщений за несколько секунд. --retry у curl учитывает Retry-After; в плагине либо копите события и отправляйте один embed раз в несколько секунд, либо смиритесь с потерей части сообщений о входе. Заголовки разобраны в статье про лимиты запросов.
  • Сервер лагает, когда Discord медленно отвечает. Плагин отправил запрос в главном потоке. Используйте sendAsync или runTaskAsynchronously.
  • Игроки подделывают вход через чат. Если игрок напишет в чат Steve joined the game, в логе это будет <Alex> Steve joined the game. Проверка на < в скрипте такие строки отбрасывает — не убирайте её.

FAQ

Нужен ли Discord-бот для уведомлений Minecraft-сервера?

Нет. Вебхук закрывает односторонние уведомления: вход, выход, смерть, достижения, статус онлайн/офлайн. Бот (DiscordSRV или свой) нужен только для двустороннего чата, доступа к консоли или команд из Discord.

Работает ли скрипт по логу на Fabric, Forge и модпаках?

Да. Он читает logs/latest.log, куда любой сервер Java Edition пишет одинаковые строки joined the game и left the game. Отличается только префикс перед сообщением, и скрипт его отрезает.

Можно ли отправлять в тред или пост форума вместо канала?

Да. Добавьте ?thread_id=<id> к URL вебхука для существующего треда или передайте thread_name в теле для форум-канала — создастся новый пост. Подробнее в статье о тредах и форумах.

Упрутся ли сообщения о входе и выходе в лимит запросов?

На маленьком сервере — нет. На большом могут при массовом переподключении; объединяйте события в один embed или делайте паузу при 429, как описано выше.

Что дальше

Начните со скрипта по логу — он работает на любом сервере уже сегодня, — а на плагин переходите, когда захочется обойтись без внешнего процесса. Соберите embed в бесплатном конструкторе Discord Webhook, экспортируйте JSON и вставьте в выбранный вариант. Тот же приём для других платформ: Roblox в Discord, автоматизация уведомлений и руководство по настройке вебхука.

Теги: minecraftdiscord webhookвебхук discordуведомления minecraftплагин paperdiscordsrvpterodactyl

Похожие статьи

Все статьи

Соберите это в визуальном редакторе

Embed, Components V2, кнопки и опросы с живым предпросмотром Discord. Бесплатно, без регистрации.