Minecraft Discord Webhook: уведомления о входе, выходе и смерти
Уведомления Minecraft-сервера в Discord через webhook: вход, выход и смерти — плагин для Paper, bash-скрипт по логу для vanilla, cron и Pterodactyl. Без бота.
На этой странице
- Бот или webhook: что выбрать
- Создаём webhook
- Вариант 1: плагин для Paper — вход, выход и смерть
- plugin.yml и config.yml
- Класс плагина
- Сборка и установка
- Вариант 2: читаем latest.log на vanilla, Fabric и Forge
- Скрипт
- Запускаем как сервис
- Pterodactyl и cron
- Pterodactyl
- Cron: статус раз в час через RCON
- Пинг роли, когда сервер упал
- Частые ошибки
- FAQ
- Нужен ли Discord-бот для уведомлений Minecraft-сервера?
- Работает ли скрипт по логу на Fabric, Forge и модпаках?
- Можно ли отправлять в тред или пост форума вместо канала?
- Упрутся ли сообщения о входе и выходе в лимит запросов?
- Что дальше
Комьюнити 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, автоматизация уведомлений и руководство по настройке вебхука.