Bot de Telegram como administrador del sistema del servidor / Sudo Null IT News

Al iniciar un servidor, a menudo es necesario proporcionar acceso a parte de la funcionalidad a otros usuarios, mientras que los propios usuarios pueden no tener las competencias suficientes para utilizar completamente el software y/o queremos limitar el conjunto de comandos disponibles.

Una solución es un bot de Telegram, que actúa como una capa entre el usuario y el software. Ya me he encontrado con esta solución en la experiencia real al menos dos veces y, basándome en una de ellas, intentaré explicar cómo se puede hacer.

Descargo de responsabilidad

De profesión, soy programador de C++ para Unreal Engine, y la administración de servidores y devops son parte de un proyecto favorito y de interés en el área, por lo que si conoce otros métodos o formas de mejorar lo siguiente, estaré encantado de conversar en el comentarios y/o personalmente.

Administrador del sistema de ensueño.  Funciona a kilovatios por hora.

Administrador del sistema de ensueño. Funciona a kilovatios por hora.

Introducción

Como ya dije, tengo dos ejemplos en mi experiencia.

El primer ejemplo es un proyecto comercial en el que no tuve tiempo de participar, pero conozco la parte arquitectónica. Una de las características principales era la posibilidad de que los artistas agregaran contenido a la aplicación después del lanzamiento sin la intervención del desarrollador. Para hacer esto, los artistas enviaron contenido en un formato especial a un bot de Telegram, y este les creó una nueva versión de la aplicación.

El segundo ejemplo es de la serie “hágalo usted mismo”. Recientemente lancé mi servidor doméstico en Ubuntu, que describí brevemente en una publicación. En este servidor inicié un servidor de Minecraft en el que jugaba con amigos. Para asegurarme de que la computadora servidor no estuviera constantemente cargada con el servidor del juego, necesitaba darles a mis amigos la oportunidad de encender y apagar el servidor sin problemas sin mi participación si querían jugar durante una hora sin mí. En consecuencia, decidí aprovechar la oportunidad y estudié cómo se puede hacer esto usando un bot de Telegram.

Basándonos en la creación del segundo bot, veremos cómo escribir y ejecutar dicha solución.

robot de telegrama

Internet está lleno de guías de inicio para crear bots de Telegram en Python, por lo que escribiré brevemente sobre la base del bot. Yo mismo encontré mucha información a partir de este artículo y libros en línea de la biblioteca de aiogramas.

Configurando el ambiente

Lo primero que hacemos es crear un directorio donde se ubicarán nuestro(s) bot(s), en el cual crearemos un entorno virtual e instalaremos aiogram y python-dotenv para los archivos de configuración.

mkdir telegram-bots
cd telegram-bots/
# Создание виртуального окружения
python3 -m venv bots-venv 
# Входим в виртуальное окружение
. bots-venv/bin/activate
# Установка библиотек
pip install aiogram python-dotenv pydantic pydantic_settings
# Выходим из виртуального окружения
deactivate

Creé un subdirectorio separado para el bot.

mkdir minecraft-bot
cd minecraft-bot

Encontré el robot

Primero, escribamos la base del bot que responderá al comando. /start y muestre el teclado para controlarlo.

Creemos tres archivos minecraft_bot.py con el código del bot principal, config_reader.py para leer el archivo .env y el propio .env para almacenar el token del bot, que se obtiene de BotPadre.

minecraft_bot.py:

minecraft_bot.py Base
# импорты
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
Primera comunicación con un bot

Primera comunicación con un bot

Para comenzar ejecutamos el comando python3 minecraft_bot.py en un entorno virtual. Como resultado, todavía no tenemos el robot más inteligente, pero que ya funciona, con botones convenientes y respuestas al presionarlos.

scripts de bash

Entonces, déjame recordarte que estamos creando un bot para administrar el servidor, es decir, necesitamos poder iniciar procesos y monitorear si están funcionando o no. Para hacer esto, necesita aprender a ejecutar comandos bash desde un programa Python (usaremos el sistema operativo y las bibliotecas de subprocesos) y también escribir scripts bash para detener e iniciar el servidor de Minecraft. Creamos todos los scripts bash en el mismo directorio que el código del bot y no olvidemos darles permiso para ejecutarse. sudo chmod +x *.sh.

Comencemos con los scripts bash inicio-servidor.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  

I detener-servidor.sh:

#!/bin/bash
# Проверяем что существует процесс с pid переданном в первом аргументе и убиваем его
if ps -p $1 > /dev/null
then
    echo "Trying to stop pid $1"
    kill $1
else
    echo "No pid"
fi

Para comprobar el funcionamiento del proceso utilizamos la biblioteca del sistema operativo:

import os

def check_pid(pid):        
    try:
        os.kill(pid, 0)
    except OSError:
        return False
    else:
        return True

Para ejecutar scripts bash – subproceso:

# Запуск скрипта старта сервера и запись вывода в переменную result
result = subprocess.run(("sh", "./start-server.sh"), capture_output=True, text=True).stdout
# Запуск скрипта остановки сервера (процесса с pid)
subprocess.call(("sh", "./stop-server.sh", pid))

Almacenamiento de datos

Lo último que necesitamos para ejecutar el bot es almacenar el pid del proceso en ejecución. Pid es la abreviatura de ID de proceso, es decir, el número único del proceso que se está ejecutando actualmente en el sistema. Se podría usar una variable global para almacenar pid, pero estoy en contra de las variables globales no constantes. Por lo tanto, escribiremos todo en un archivo json usando la biblioteca del mismo nombre (si hay más datos, puedes conectar la base de datos):

Usando json para almacenar 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))

Aquí está el código completo del bot resultante:

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())

Inicio automático del robot

El toque final de este proyecto es el inicio automático de nuestro bot cuando se enciende el servidor Ubuntu y la limpieza de procesos en caso de fallas inesperadas del bot. Para hacer esto, recurramos nuevamente a los scripts de bash y escribamos un servicio que los ejecute.

inicio.sh Para iniciar un bot de Telegram, todos los comandos ya se han comentado anteriormente:

#!/bin/bash
cd /home/user/telegram-bots
. bots-venv/bin/activate
cd minecraft-bot
python3 minecraft_bot.py

parada.sh para detener el servidor si el bot falla (aquí es donde el pid registrado en el archivo server.pid resultó útil)

#!/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

Ahora todo lo que queda es escribir un servicio que se iniciará usando systemd, crear un archivo /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

Aquí hemos indicado qué scripts ejecutar al inicio (ExecStart) y pare (ExecStop) servicio. También indicaron que el servicio debe reiniciarse si falla (Restart=on-failure) e intenta hacer esto cada 5 segundos (RestartSec=5s).

Luego ejecutamos los siguientes comandos para iniciar el servicio:

sudo systemctl daemon-reload
sudo systemctl enable minecraft-bot.service
sudo systemctl start minecraft-bot.service

En este punto, la creación del bot se puede considerar completa.

Conclusión

En este artículo quería reflejar todo el proceso de creación de un bot de Telegram para administrar el servidor Ubuntu, es decir, usando el ejemplo de iniciar, detener y verificar el funcionamiento del servidor de Minecraft.

Las ventajas de esta solución son que:

  • Interfaz amigable. El usuario no necesita ser un genio de Linux ni de la programación para interactuar con el software en el servidor.

  • Fácil acceso. No es necesario tener acceso vía IP, ya que todas las solicitudes pasan por el servidor de Telegram. Además, el bot permite trabajar desde cualquier dispositivo con Telegram instalado

  • Control de acceso flexible. El desarrollador controla completamente la funcionalidad disponible para el usuario, y también puede ingresar a una base de datos con autorización para diferentes niveles de acceso.

Entre las desventajas puedo destacar:

  • Complejidad de la solución. Se puede requerir mucho trabajo para crear un bot en sí, especialmente con una lógica más compleja, lo que puede ser menos rentable que capacitar al usuario para trabajar con funciones sin una capa hermosa.

PD

El material fue recopilado mediante autoestudio, por lo que siempre agradeceré sugerencias de mejora en comentarios o comunicación personal.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *