При запуске сервера часто необходимо предоставить доступ к части функционала другим пользователям, при этом сами пользователи могут не иметь достаточных компетенций для полноценного использования софта и/или мы хотим ограничить доступный набор команд.
Одним из вариантов решения является Telegram-бот, который является прослойкой между пользователем и софтом. С таким решением я встречался на реальном опыте уже как минимум два раза, и на основе одного из них я постараюсь объяснить, как это можно сделать.
Дисклеймер
По специальности я программист C++ для Unreal Engine, а девопс и администрирование сервера — это часть пет-проекта и интереса к области, поэтому если вы знаете другие методы или способы по улучшению приведённого ниже, буду рад пообщаться в комментариях и/или лично.

Введение
Как я уже сказал у меня в опыте есть два примера.
Первый пример — это коммерческий проект, в котором я не успел поучаствовать, но знаю архитектурную часть. Одной из основных функций являлась возможность добавления контента в приложение художниками уже после релиза без вмешательства разработчиков. Для этого художники отправляли контент в специальном формате телеграм боту, и он им собирал новую версию приложения.
Второй пример уже из серии "сделай сам для себя". Недавно я запустил свой домашний сервер на Ubuntu, о котором вкратце рассказал в публикации. На этом сервере я запустил сервер Minecraft, на котором играл с друзьями. Чтобы серверный компьютер не был постоянно нагружен игровым сервером, мне нужно было дать возможность друзьям безболезненно включать и выключать сервер без моего участия, если они захотели часок другой поиграть без меня. Соответственно я решил воспользоваться возможно��тью и изучил как это можно сделать с помощью телеграм бота.
Вот на основе создания второго бота и будем смотреть, как написать и запустить подобное решение.
Telegram-бот
На просторах интернета полно стартовых гайдов по созданию телеграм ботов на Python, поэтому про основу бота буду писать кратко. Сам я нашёл много информации начиная с этой статьи и онлайн книги по библиотеке aiogram.
Настройка окружения
Первое, что мы делаем это создаёт директорию где будут лежать наши бот(ы), в котором создадим виртуальное окружение и установим aiogram и python-dotenv для файлов конфигурации.
mkdir telegram-bots cd telegram-bots/ # Создание виртуального окружения python3 -m venv bots-venv # Входим в виртуальное окружение . bots-venv/bin/activate # Установка библиотек pip install aiogram python-dotenv pydantic pydantic_settings # Выходим из виртуального окружения deactivate
Для бота я создал отдельную поддиректорию
mkdir minecraft-bot cd minecraft-bot
Основа бота
Для начала напишем основу бота, которая будет отвечать на команду /start и выводить клавиатуру для управления.
Создадим три файла minecraft_bot.py c основным кодом бота, config_reader.py для чтения .env файла и сам .env для хранения токена бота, который получается от BotFather.
minecraft_bot.py:
minecraft_bot.py Основа
# импорты import asyncio import logging from aiogram import Bot, Dispatcher, types, F from aiogram.filters.command import Command from config_reader import config # Включаем логирование, чтобы не пропустить важные сообщения logging.basicConfig(level=logging.INFO) # Бот с токеном из конфига bot = Bot(token=config.bot_token.get_secret_value()) # Диспетчер, получающий апдейты телеграмма dp = Dispatcher() # Хэндлер на команду /start @dp.message(Command("start")) async def cmd_start(message: types.Message): # Клавиатура с тремя кнопками kb = [ [types.KeyboardButton(text="Check server status")], [types.KeyboardButton(text="Start server")], [types.KeyboardButton(text="Stop server")], ] keyboard = types.ReplyKeyboardMarkup(keyboard=kb) await message.answer("Hi! \nThis bot can tell you server status and help you to start and stop it", reply_markup=keyboard) # Хэндлеры кнопок @dp.message(F.text.lower() == "check server status") async def check_server_status(message: types.Message): await message.answer("Can't Check Server Status") @dp.message(F.text.lower() == "start server") async def check_server_status(message: types.Message): await message.answer("Starting server") @dp.message(F.text.lower() == "stop server") async def check_server_status(message: types.Message): await message.answer("Stopping server") # Запуск процесса поллинга новых апдейтов async def main(): await dp.start_polling(bot) if __name__ == "__main__": asyncio.run(main())
config_reader.py:
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic import SecretStr class Settings(BaseSettings): # Желательно вместо str использовать SecretStr # для конфиденциальных данных, например, токена бота bot_token: SecretStr # Начиная со второй версии pydantic, настройки класса настроек задаются # через model_config # В данном случае будет использоваться файла .env, который будет прочитан # с кодировкой UTF-8 model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8') # При импорте файла сразу создастся # и провалидируется объект конфига, # который можно далее импортировать из разных мест config = Settings()
.env:
BOT_TOKEN = 12345678:AaBbCcDdEeFfGgHh

Для запуска выполняем команду python3 minecraft_bot.py в виртуальном окружении. В результате у нас есть ещё не самый умный, но уже рабочий бот, с удобными кнопками и ответами на их нажатие.
Bash скрипты
Итак, напомню, что мы создаём бота для управления сервером, то есть нам нужно уметь запускать процессы и отслеживать работают они или нет. Для этого нужно научиться запускать bash команды из программы Python (воспользуемся билиотеками os и subprocess), а также написать bash скрипты для остановки и запуска сервера Minecraft. Все bash скрипты создаём в той же директори, что и код для бота, и не забываем прописывать им разрешение на запуск sudo chmod +x *.sh.
Начнём с bash скриптов start-server.sh:
#!/bin/bash # Переходим в директорию сервера cd /home/user/NewServer # Запускаем сервер в бекграунде, перенаправляем его вывод в nohup.output и отвязываем связь с скриптом nohup java @user_jvm_args.txt @libraries/net/minecraftforge/forge/1.18.2-40.2.9/unix_args.txt nogui > nohup.output 2>&1 & # Выводим pid последнего запущенного процесса (сервера) в консоль и в файл server.pid echo $! echo $! > /home/user/telegram-bots/minecraft-bot/server.pid
И stop-server.sh:
#!/bin/bash # Проверяем что существует процесс с pid переданном в первом аргументе и убиваем его if ps -p $1 > /dev/null then echo "Trying to stop pid $1" kill $1 else echo "No pid" fi
Для проверки работы процесса используем библиотеку os:
import os def check_pid(pid): try: os.kill(pid, 0) except OSError: return False else: return True
Для запуска bash скриптов - subprocess:
# Запуск скрипта старта сервера и запись вывода в переменную result result = subprocess.run(["sh", "./start-server.sh"], capture_output=True, text=True).stdout # Запуск скрипта остановки сервера (процесса с pid) subprocess.call(["sh", "./stop-server.sh", pid])
Хранение данных
Последний момент, которого нам не хватает для запуска бота — это хранение pid'а запущенного процесса. Pid — это сокращение от process id, то есть уникальный номер процесса который сейчас исполняется в системе. Для хранения pid можно было бы использовать глобальную переменную, но я противник не константных глобальных переменных. Поэтому запишем всё в файл json, с помощью одноименной библиотеки (при большем количестве данных можно подключать базу данных):
Использование json для хранения pid
import json import os.path # Имя json-файла и переменной в нём SERVER_DATA_PATH = 'minecraft_server.json' PID = "pid" # Получение всех данных из json-файла def get_server_data(): if os.path.isfile(SERVER_DATA_PATH): with open(SERVER_DATA_PATH, 'r') as openfile: return json.load(openfile) else: return {} # Запись всех данных в json-файла def set_server_data(server_data): with open(SERVER_DATA_PATH, 'w') as f: json.dump(server_data, f) # Запись всех данных в json-файла с изменённым значением def set_server_data_value(key, value): server_data = get_server_data() server_data[key] = value set_server_data(server_data) def check_pid(pid): try: os.kill(pid, 0) except OSError: return False else: return True # Проверка запущен ли сервер def check_server_process(): server_data = get_server_data() if not PID in server_data: return False if server_data[PID] == -1: return False return check_pid(server_data[PID])
Вот полный код полученного бота:
minecraft_bot.py
# импорты import asyncio import logging from aiogram import Bot, Dispatcher, types, F from aiogram.filters.command import Command from config_reader import config import subprocess import json import os.path import os # Имя json-файла и переменной в нём SERVER_DATA_PATH = 'minecraft_server.json' PID = "pid" # Получение всех данных из json-файла def get_server_data(): if os.path.isfile(SERVER_DATA_PATH): with open(SERVER_DATA_PATH, 'r') as openfile: return json.load(openfile) else: return {} # Запись всех данных в json-файла def set_server_data(server_data): with open(SERVER_DATA_PATH, 'w') as f: json.dump(server_data, f) # Запись всех данных в json-файла с изменённым значением def set_server_data_value(key, value): server_data = get_server_data() server_data[key] = value set_server_data(server_data) def check_pid(pid): try: os.kill(pid, 0) except OSError: return False else: return True # Проверка запущен ли сервер def check_server_process(): server_data = get_server_data() if not PID in server_data: return False if server_data[PID] == -1: return False return check_pid(server_data[PID]) # Включаем логирование, чтобы не пропустить важные сообщения logging.basicConfig(level=logging.INFO) # Бот с токеном из конфига bot = Bot(token=config.bot_token.get_secret_value()) # Диспетчер, получающий апдейты телеграмма dp = Dispatcher() # Хэндлер на команду /start @dp.message(Command("start")) async def cmd_start(message: types.Message): # Клавиатура с тремя кнопками kb = [ [types.KeyboardButton(text="Check server status")], [types.KeyboardButton(text="Start server")], [types.KeyboardButton(text="Stop server")], ] keyboard = types.ReplyKeyboardMarkup(keyboard=kb) await message.answer("Hi! \nThis bot can tell you server status and help you to start and stop it", reply_markup=keyboard) # Хэндлеры кнопок @dp.message(F.text.lower() == "check server status") async def check_server_status(message: types.Message): if check_server_process(): await message.answer("Server is running") return await message.answer("Server is stopped") @dp.message(F.text.lower() == "start server") async def check_server_status(message: types.Message): if check_server_process(): await message.answer("Server is already running") return result = subprocess.run(["sh", "./start-server.sh"], capture_output=True, text=True).stdout set_server_data_value(PID, int(result)) await message.answer("Starting server") @dp.message(F.text.lower() == "stop server") async def check_server_status(message: types.Message): server_data = get_server_data() if check_server_process(): subprocess.call(["sh", "./stop-server.sh", str(server_data[PID])]) set_server_data_value(PID, -1) await message.answer("Stoping server") return await message.answer("Server is already stopped") # Запуск процесса поллинга новых апдейтов async def main(): await dp.start_polling(bot) if __name__ == "__main__": asyncio.run(main())
Автозапуск бота
Последний штрих этого проекта — это автозапуск нашего бота при включении Ubuntu сервера и чистка процессов при неожиданных вылетах бота. Для этого снова обратимся к bash скриптам и напишем сервис, который будет их запускать.
start.sh для запуска телеграм бота, все команды уже были рассмотрены ранее:
#!/bin/bash cd /home/user/telegram-bots . bots-venv/bin/activate cd minecraft-bot python3 minecraft_bot.py
stop.sh для остановки сервера в случае падения бота (вот тут нам и пригодился pid записанный в файл server.pid)
#!/bin/bash cd /home/user/telegram-bots/minecraft-bot # Читаем pid Minecraft сервера server_pid=$(head -n 1 server.pid) # Останавливаем Minecraft сервер и удаляем файлы с pid данными sh ./stop-server.sh "$server_pid" rm minecraft_server.json rm server.pid
Теперь осталось только написать сервис, который будет запускаться с помощью systemd, создадим файл /etc/systemd/system/minecraft-bot.service:
[Unit] Description=Minecraft Server Telegram Bot [Service] ExecStart=/home/user/telegram-bots/minecraft-bot/start.sh ExecStop=/home/user/telegram-bots/minecraft-bot/stop.sh Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target
Здесь мы указали какие скрипты запускать при запуске (ExecStart) и остановке (ExecStop) сервиса. Также прописали, что сервис необходимо перезапустить при его падении (Restart=on-failure) и пробовать это сделать каждые 5 секунд (RestartSec=5s).
После чего выполняем следующие команды для запуска сервиса:
sudo systemctl daemon-reload sudo systemctl enable minecraft-bot.service sudo systemctl start minecraft-bot.service
Вот на этом моменте создание бота можно считать оконченным.
Заключение
В этой статье я хотел отразить весь процесс создания Telegram-бота для администрирования сервера Ubuntu, а именно на примере запуска, остановки и проверки работы сервера Minecraft.
Плюсы данного решения заключаются в том, что:
Дружелюбный интерфейс. Пользователю не надо быть гением linux и программирования, чтобы взаимодействовать с софтом на сервере.
Лёгкий доступ. Не надо иметь доступ по ip, так как все запросы идут через сервер Telegram. Кроме этого бот позволяет работать с любого устройство с установленным Telegram
Гибкий контроль доступа. Разработчик полностью контролирует функционал доступный пользователю, а также можно ввести базу данных с авторизацией для разных уровней доступа.
Из минусов могу выделить:
Сложность решения. Много трудозатрат может уйти на само создание бота, особенно с более сложно логикой, что может быть менее выгодно, чем обучить пользователя работать с функционалом без красивой прослойки.
P.S.
Материал был собран на основе самостоятельного обучения, поэтому всегда буду рад предложениям по улучшению в комментариях или личном общении.
