Telegram-бот с уведомлениями о новых игроках
Держите приватный сервер для своих, но время от времени в онлайне мелькает ник, которого никто не звал — то ли IP слили, то ли сервер как-то попал в паблик-листы сканеров. Смотреть логи руками каждый вечер не вариант, а готовые паблик-мониторинги показывают только суммарный онлайн, а не кто именно зашёл. Решение простое: бот, который сам читает логи сервера, сверяет ник или SteamID с вашим списком «своих» и присылает в Telegram сообщение только тогда, когда объявился кто-то новый.
Содержание
Что мы строим и зачем это нужно
Идея не в полноценной системе мониторинга (это отдельная задача, для неё есть Server Query API и опрос через query-протокол), а в узкой, но полезной штуке: тихий фоновый процесс, который следит за событиями подключения в логах игрового сервера и молчит, пока заходят только знакомые. Как только в логе появляется ник или SteamID, которого нет в вашем списке — прилетает сообщение в личку или в чат админов с ником, временем и (если доступно) IP/SteamID.
Это не замена whitelist — вайтлист сам по себе не пускает чужих, и если он уже включён, бот скорее для серверов, где вайтлист принципиально не нужен (открытый для друзей и друзей друзей сервер, куда пускают всех, но хочется знать, кто зашёл). Второй сценарий — вайтлист есть, но вы хотите видеть сам факт попытки подключения чужака (в Minecraft это строка с причиной «not whitelisted» — сигнал, что кто-то узнал IP и пробует зайти).
Из чего состоит система:
- Источник событий — серверный лог (или системный журнал через
journalctl, если сервер под systemd). - Парсер — скрипт, который читает лог построчно и матчит строки подключения по регулярке.
- Хранилище известных игроков — простой JSON или SQLite со списком SteamID/UUID/ников, которых бот считает «своими».
- Telegram-бот — отправляет сообщение через Bot API, когда парсер находит незнакомца.
Если у вас ещё нет запущенного бота на VPS — сначала пройдите базовую установку Telegram-бота: там про BotFather, токен, python-telegram-bot/aiogram и автозапуск через systemd. Здесь считаем, что бот уже живёт 24/7, и добавляем ему только эту функцию.
Где искать события подключения в логах
Формат строки подключения у каждой игры свой, и тут нет универсального решения — разберём три частых случая.
Minecraft (Vanilla/Paper/Spigot). Лог лежит в logs/latest.log, актуальная строка входа выглядит примерно так:
[12:34:56] [Server thread/INFO]: Steve[/91.203.XX.XX:54321] logged in with entity id 123 at (120.5, 64.0, -80.2)
Для перехвата достаточно регулярки на паттерн logged in with entity id — ник и IP уже в той же строке. У Paper/Spigot формат идентичен ванильному, отличия только в деталях координат.
Source-игры (CS2, Rust, Garry's Mod, TF2). Лог событий пишется в console.log или через встроенный logging-аддон, строка подключения выглядит так (Rust, server.log):
14:32:07 | 76561198012345678/PlayerNick joined [connected]
Здесь сразу есть SteamID64 — удобнее, чем ник, потому что ник можно сменить, а SteamID у аккаунта постоянный. В CS2 через встроенный log on в консоли сервера строки подключения приходят в похожем формате с STEAM_1:... (Steam2 ID) — конвертировать в SteamID64 можно по стандартной формуле SteamID64 = 76561197960265728 + (Y + Z*2), но проще сразу брать готовые библиотеки конвертации (steamid на npm, steam на PyPI).
ARK: Survival. Подключения видно через RCON-команду listplayers (сравнение списков между опросами) или в ShooterGame.log по строке SERVER: Auth Session request completed — но надёжнее опрашивать RCON раз в 20-30 секунд и сверять список онлайна с предыдущим снимком: игры на Unreal Engine не всегда пишут подключение в лог явно.
Для игр без чёткой лог-строки (модовые сборки, кастомные плагины) универсальный запасной вариант: RCON + периодический опрос списка игроков (listplayers, players, status) и сравнение с предыдущим снимком — новый ник в списке, которого не было секунду назад, и есть событие подключения. Чуть грубее логов (задержка равна интервалу опроса), но не зависит от формата конкретного лога.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверХвост лога в реальном времени
Читать лог целиком при каждой проверке — плохая идея на активном сервере, файл растёт быстро. Правильный подход — держать позицию последнего прочитанного байта и дочитывать только новое, как tail -f, но из кода бота.
Python-пример с watchdog (следит за изменением файла и не гоняет CPU впустую опросом):
import re
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
LOG_PATH = "/home/minecraft/server/logs/latest.log"
JOIN_PATTERN = re.compile(r"(\w+)\[/([\d.]+):\d+\] logged in")
class LogHandler(FileSystemEventHandler):
def __init__(self):
self.position = self._file_size()
def _file_size(self):
try:
with open(LOG_PATH, "r") as f:
f.seek(0, 2)
return f.tell()
except FileNotFoundError:
return 0
def on_modified(self, event):
if event.src_path != LOG_PATH:
return
with open(LOG_PATH, "r", errors="ignore") as f:
f.seek(self.position)
new_lines = f.readlines()
self.position = f.tell()
for line in new_lines:
match = JOIN_PATTERN.search(line)
if match:
nick, ip = match.group(1), match.group(2)
handle_join(nick, ip)
observer = Observer()
observer.schedule(LogHandler(), path="/home/minecraft/server/logs", recursive=False)
observer.start()
Нюанс с ротацией лога: Minecraft каждый рестарт создаёт новый latest.log, а старый архивирует в logs/2026-08-30-1.log.gz. Если сервер перезапускается, а position в коде остался от старого файла — обработчик может либо застрять, либо пропустить строки сразу после старта. Проверяйте размер файла при каждом чтении: если он меньше сохранённой позиции — файл пересоздан, сбрасывайте position в 0.
Для Source-игр логика та же, просто путь и регулярка другие — console.log в папке сервера, и паттерн под конкретную игру (см. примеры строк выше).
Список известных игроков и логика сравнения
Простейшее хранилище — JSON-файл, который вы редактируете руками или через команду бота:
{
"known_players": [
{"nick": "Steve", "steamid": null, "added": "2026-01-15"},
{"nick": "PlayerNick", "steamid": "76561198012345678", "added": "2026-02-03"}
]
}
Для сравнения приоритет должен быть у SteamID/UUID, а не у ника — ники меняются, а идентификатор аккаунта нет. Если в логе доступен только ник (как в ванильном Minecraft без включённого online-mode) — сверяйте по нику, но учитывайте, что это слабее: сменивший ник знакомый игрок один раз даст ложный алерт, а чужой под похожим ником может проскочить.
import json
def load_known():
with open("known_players.json") as f:
return json.load(f)["known_players"]
def is_known(nick, steamid=None):
known = load_known()
for p in known:
if steamid and p.get("steamid") == steamid:
return True
if p["nick"].lower() == nick.lower():
return True
return False
def handle_join(nick, ip, steamid=None):
if not is_known(nick, steamid):
send_telegram_alert(nick, ip, steamid)
Для Minecraft с включённым online-mode=true в server.properties в логе можно достать и UUID игрока (более надёжный идентификатор, чем ник) — он появляется в отдельной строке аутентификации чуть раньше строки logged in, стоит матчить обе и связывать по времени/потоку.
Отправка уведомления в Telegram
Сама отправка — вызов Bot API sendMessage, ничего сложного, если бот уже настроен и у вас есть chat_id (личный чат с ботом или ID группы админов):
import requests
from datetime import datetime
BOT_TOKEN = "ваш_токен_из_BotFather"
CHAT_ID = "ваш_chat_id"
def send_telegram_alert(nick, ip, steamid=None):
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
text = (
f"🟡 Новый игрок на сервере\n"
f"Ник: {nick}\n"
f"Время: {now}\n"
)
if steamid:
text += f"SteamID: {steamid}\n"
if ip:
text += f"IP: {ip}\n"
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
resp = requests.post(url, data={"chat_id": CHAT_ID, "text": text})
if resp.status_code != 200:
print(f"Ошибка отправки в Telegram: {resp.text}")
Пара практических деталей, которые обычно вылезают на второй день работы бота:
- Дедупликация. Если игрок несколько раз реконнектится (нестабильный интернет, краш клиента), бот будет слать алерт на каждый вход. Добавьте кэш «уже уведомляли этого игрока в последние N минут» — простой словарь
{nick: timestamp}в памяти процесса. - Хранение IP — с оглядкой на приватность. IP-адрес — персональные данные; если бот пишет их в чат и хранит в логах, прочитайте статью про хранение персональных данных игроков — там про то, сколько реально нужно хранить.
- Rate limit Telegram. На один чат — не больше ~1 сообщения в секунду. Для точечных алертов это почти никогда не проблема, но если вешаете бота на десяток серверов сразу — добавьте очередь с паузой между отправками.
Автоматическое пополнение списка «своих»
Ручное редактирование JSON быстро надоедает, если у вас реально приватный сервер с текучкой из десятка друзей. Удобнее сделать команду боту, которая добавляет игрока в список известных прямо из Telegram — например, ответом на само уведомление:
from telegram import Update
from telegram.ext import ContextTypes
async def approve_player(update: Update, context: ContextTypes.DEFAULT_TYPE):
# ожидаем команду вида /approve PlayerNick
if not context.args:
await update.message.reply_text("Использование: /approve <ник>")
return
nick = context.args[0]
known = load_known()
known.append({"nick": nick, "steamid": None, "added": str(datetime.now().date())})
with open("known_players.json", "w") as f:
json.dump({"known_players": known}, f, ensure_ascii=False, indent=2)
await update.message.reply_text(f"{nick} добавлен в список известных игроков.")
Так уведомление превращается в мини-workflow: пришёл алерт → это друг под новым ником → команда /approve НикДруга → бот больше не напомнит о нём. Для группового чата админов ограничьте команду проверкой user_id, чтобы approve не мог вызвать кто попало.
Запуск как systemd-сервис
Парсер логов и Telegram-бот удобно держать одним процессом, который стартует вместе с сервером и переживает его рестарты. Пример юнита:
# /etc/systemd/system/join-watcher.service
[Unit]
Description=Telegram-уведомления о новых игроках
After=network.target
[Service]
Type=simple
User=minecraft
WorkingDirectory=/home/minecraft/join-watcher
ExecStart=/usr/bin/python3 /home/minecraft/join-watcher/bot.py
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now join-watcher
sudo systemctl status join-watcher
Если процесс упадёт (лог-файл временно недоступен из-за ротации), Restart=on-failure поднимет его заново через 10 секунд без ручного вмешательства. Логи бота смотрите через journalctl -u join-watcher -f — это первое место для диагностики, если алерты перестали приходить; общая методика поиска причины та же, что в статье про чтение логов и краш-репортов, просто применённая к своему скрипту, а не к игровому серверу.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверЧастые вопросы
Бот будет грузить сервер, если постоянно читает лог?
Нет, если реализовано через tail-подобное чтение с отслеживанием позиции (как в примере с watchdog) — читаются только новые байты, а не файл целиком. Опрос через RCON раз в 20-30 секунд тоже практически не заметен для сервера.
Что если сервер не пишет IP в лог (например, из соображений приватности отключено)?
Тогда сверяйтесь только по нику или SteamID — большинство игр всё равно пишут хотя бы один из этих идентификаторов при подключении, IP не обязателен для самой идеи «свой/чужой».
Можно ли повесить одного бота на несколько серверов сразу?
Да, заведите словарь конфигураций (путь к логу или RCON-доступ + свой файл известных игроков на каждый сервер) и добавьте название сервера в текст уведомления, чтобы не путать, откуда пришёл алерт.
А если игрок использует чужой ник специально, чтобы обмануть бота?
Смена ника не подделывает SteamID/UUID, поэтому если сверяете именно по нему — обман не пройдёт. Сверка только по нику действительно уязвима к такому — используйте её как резервный вариант, а не основной.
Нужно ли останавливать бота, когда на сервере пусто?
Не обязательно: пустой сервер просто не генерирует событий подключения, процесс в это время не потребляет заметных ресурсов, можно оставить его работать постоянно.