MAATRIX GAMES / Блог / Telegram-бот с уведомлениями о новых игроках

Telegram-бот с уведомлениями о новых игроках

MAATRIX GAMES

Держите приватный сервер для своих, но время от времени в онлайне мелькает ник, которого никто не звал — то ли IP слили, то ли сервер как-то попал в паблик-листы сканеров. Смотреть логи руками каждый вечер не вариант, а готовые паблик-мониторинги показывают только суммарный онлайн, а не кто именно зашёл. Решение простое: бот, который сам читает логи сервера, сверяет ник или SteamID с вашим списком «своих» и присылает в Telegram сообщение только тогда, когда объявился кто-то новый.

Что мы строим и зачем это нужно

Идея не в полноценной системе мониторинга (это отдельная задача, для неё есть Server Query API и опрос через query-протокол), а в узкой, но полезной штуке: тихий фоновый процесс, который следит за событиями подключения в логах игрового сервера и молчит, пока заходят только знакомые. Как только в логе появляется ник или SteamID, которого нет в вашем списке — прилетает сообщение в личку или в чат админов с ником, временем и (если доступно) IP/SteamID.

Это не замена whitelist — вайтлист сам по себе не пускает чужих, и если он уже включён, бот скорее для серверов, где вайтлист принципиально не нужен (открытый для друзей и друзей друзей сервер, куда пускают всех, но хочется знать, кто зашёл). Второй сценарий — вайтлист есть, но вы хотите видеть сам факт попытки подключения чужака (в Minecraft это строка с причиной «not whitelisted» — сигнал, что кто-то узнал IP и пробует зайти).

Из чего состоит система:

  1. Источник событий — серверный лог (или системный журнал через journalctl, если сервер под systemd).
  2. Парсер — скрипт, который читает лог построчно и матчит строки подключения по регулярке.
  3. Хранилище известных игроков — простой JSON или SQLite со списком SteamID/UUID/ников, которых бот считает «своими».
  4. 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, поэтому если сверяете именно по нему — обман не пройдёт. Сверка только по нику действительно уязвима к такому — используйте её как резервный вариант, а не основной.

Нужно ли останавливать бота, когда на сервере пусто?

Не обязательно: пустой сервер просто не генерирует событий подключения, процесс в это время не потребляет заметных ресурсов, можно оставить его работать постоянно.