Introducción

Dende fai tempo teño un token para un bot de telegram. Pero o uso que lle daba era moi curto. Algo como pedir un producto e que me indicase o mellor prezo nalgún supermercado. Ou esa era a intención, xa que só cheguei a facer a busca nun único super. Estes días, co proxecto de auto aloxar servizos, estiven a considerar que estaría ben controlar a temperatura da Raspberry. Para facerme unha idea de que tanto tiran dela Nextcloud e outros. Tamén, no caso de que a temperatura fose demasiado elevada, poder apagar o equipo de forma remota.

Así que decidín ver como facer isto con Bash e Telegram. Algo sinxelo, sen ter que meter Python ou outra linguaxe polo medio, para algo tan simple.

Ademais, é unha boa oportunidade de practicar bash. Xa que normalmente non é unha linguaxe que utilice moito.

O script: Docker

Unha vez máis, decidín usar Docker para crear o bot de telegram. Neste caso a decisión de facelo así, foi para non instalar cousas na Raspberry, como lm-sensors. Tamén se quero meter o script noutro sitio, tampouco me teño que preocupar das dependencias. Irá todo empaquetado nun contenedor. Nun caso coma este, non é o máis eficiente, pero sí o máis cómodo.

Nesta ocasión escollín a imaxe alpine:latest como base. Sen limitar versións. Xa que, a priori, non hai nada que poida causar incompatibilidades.

O Dockerfile é o seguinte:

FROM alpine:latest

RUN apk update && \
  apk add lm-sensors curl

RUN mkdir /var/tmp/scripts/

COPY bash /var/tmp/scripts/

RUN chmod +x /var/tmp/scripts/check-temperature.sh && \
  chmod +x /var/tmp/scripts/commands/sendMessage.sh

RUN echo "*/5 * * * * /var/tmp/scripts/check-temperature.sh" > /etc/crontabs/root

CMD ["crond", "-f"]

Neste caso desbotei a opción de usar docker-compose xa que non vou vincular volumes, nin usar redes, etc.

O que podemos ver no ficheiro é o seguinte:

  • FROM alpine:latest indicamos cal vai ser a imaxe base a usar.
  • RUN apk update && \ apk add lm-sensors curl o script ten como dependencias lm-sensors e curl. Para poder medir a temperatura, chamar á API e enviar a mensaxe. Polo que actualizamos os repositorios e instalamos as dependencias.
  • RUN mkdir /var/tmp/scripts/ creamos no contenedor o directorio para os scripts.
  • COPY bash /var/tmp/scripts/ con esta liña imos copiar a carpeta bash, que está ao mesmo nivel que o noso Dockerfile, para o contenedor. Na ruta que creamos ca instrucción anterior.
  • RUN chmod +x /var/tmp/scripts/check-temperature.sh && \ chmod +x /var/tmp/scripts/commands/sendMessage.sh otorgamos permisos de execución aos scripts.
  • RUN echo “*/5 * * * * /var/tmp/scripts/check-temperature.sh” > /etc/crontabs/root aquí estamos programando o chequeo da temperatura, para que se execute cada 5 minutos.
  • CMD [“crond”, “-f”] executamos o crond.

En calquer caso, se se quixese usar o script sen docker, tamén é posible. O unico necesario é o que está no cartafol bash. Tendo en conta que precisamos que no equipo a correlo, estén instalados tanto lm-sensors como curl.

Aquí usei alpine, pero no caso de usar Debian executaríamos apt install lm-sensors curl e listo.

Cron cada 5 minutos

Imos explicar un pouco en qué consiste esa liña con tanto asterisco.
*/5 * * * * /var/tmp/scripts/check-temperature.sh

Cando queremos programar unha tarefa no cron, o formato a utilizar é este mesmo. Ímolo trocear para ver que significa:

  1. A primeira parte */5 indica os minutos. Se puxésemos só o asterisco estaríamos a indicar que sería en todo-los minutos. Se indicásemos un só número, de 0 a 59, estaríamos a indicar que se executaría nese minuto. Con */5 estamos a dicirlle que queremos que o faga, cada 5 minutos.
  2. Esta segunda parte son as horas. Aquí co asterisco, estamos a indicar que o queremos a toda-las horas. Pero, ao igual ca antes, poderíamos indicar un número de 0 a 23. Tamén unha operación coma */2 para que o faga cada dúas horas.
  3. O tercer asterisco é o día do mes, no que queremos que execute. O formato é o mesmo ca antes, * para todos, un número para o día do mes, etc.
  4. O mes. Un número de 1 a 12, * para todos, etc.
  5. O día da semana. Isto é útil, se por exemplo queremos executar algo, por exemplo, os luns a primeira hora. O luns sería o día 1, mentres que o domingo será o 0 ou o 7.
  6. O comando a executar. En este caso, o script bash.

As opcións que podemos usar en troques dos valores das datas, son as seguintes:

  1. * calquer valor
  2. , se queremos varios valores, irán separados por coma. Por exemplo os días un e quince 1,15.
  3. - se queremos un intervalo de valores. Por exemplo, para facer algo en horario laboral, 9-18. Executará o script, de nove a seis.
  4. / Este sería para os pasos, como vimos antes. Un exemplo, para executar cada 3 días, sería */3.

Supoñamos entón que queremos executar algo cada hora en punto, en horario de 0h a 8h, a primeiros e mediados de mes, de luns a venres. O resultado sería o seguinte: 0 0-8 1,15 * 1-5 commando

Para que sexa máis claro, deixo esta táboa de resumo:

Campo Valores permitidos Ejemplos
Minuto 0-59 ,/5, 0, 15, 30, 45
Hora 0-23 *, 0, 12, 18
Día del mes 1-31 *, 1, 15, 30
Mes 1-12 *, 1, 6, 12
Día de la semana 0-7 (0 y 7 son domingo) *, 1, 5, 0-5

Os scripts

A sintaxe dos scripts é unha forma abreviada de Bash, xa que en Alpine non é Bash, se non Alquimist shell que non trae toda-las instruccións.

check-temperature.sh

VERSION="0.0.1\nScript to send with telegram CPU temperature"

# Read cpu temperature using sensors
CPU_TEMPERATURE=$(sensors | grep "Core 0" | awk '{print $3}' | cut -c 2- | tr -d °)
CPU_TEMPERATURE_INT=$(echo $CPU_TEMPERATURE | tr -d .°C)
if [ $CPU_TEMPERATURE_INT -gt 700 ]; then
        MESSAGE="Raspberry CPU temperature is too high: ${CPU_TEMPERATURE}"
        # Send telegram message though sendMessage
        /var/tmp/scripts/commands/sendMessage.sh "$MESSAGE"
fi

exit 0

Neste script o que fago é indicar, a versión do mesmo e unha pequena descripción do que fai.
A continuación gardo na variable CPU_TEMPERATURE o valor da temperatura, que recolle o comando sensors. Este da máis informacion, polo que hai que filtrar. Con grep, collemos só a liña que ten Core 0. Con awk quedamos ca información da terceira columna. Con cut nos quitamos o primer caracter, dicindo que queremos dende a posición 2 en adiante. Finalmente con tr dicimos que queremos limpar os caracteres, a continuación do -d.
Na seguinte liña limpo esa variable para almacenala, coma un enteiro, xa que comparar con decimáis en Bash é máis tedioso. Así pois un valor como 34.5C, quedará como 345.
E xa na parte final, se a temperatura é maior a setecentos, que serían setenta grados. Creamos unha variable ca mensaxe que enviaremos e pasámola como argumento ao sendMessage.

sendMessage.sh

Co fin de poder utilizar o envío de mensaxes, dende outros scripts, separei este código no seu propio arquivo.

#!/bin/sh

# First check if there is any message to send
if [ $# -eq 0 ]; then
    echo "No arguments supplied"
    exit 1
fi

urlencode() {
    local string="${1}"
    local length="${#string}"
    local encoded=""
    local pos=0

    while [ $pos -lt $length ]; do
        local substring="${string:$pos:1}"
        case $substring in
        [a-zA-Z0-9._-])
            encoded="${encoded}${substring}"
            ;;
        *)
            local hex=$(printf "%02X" "'${substring}")
            encoded="${encoded}%${hex}"
            ;;
        esac
        pos=$((pos + 1))
    done

    echo "${encoded}"
}

TELEGRAM_TOKEN_FILE=/var/tmp/scripts/config/telegram-token
ALLOWED_CHATS_FILE=/var/tmp/scripts/config/allowed-chat-IDs
ALLOWED_USERS_FILE=/var/tmp/scripts/config/allowed-user-IDs

if [ ! -f "$TELEGRAM_TOKEN_FILE" ]; then
    echo "$TELEGRAM_TOKEN_FILE file does not exist"
    exit 1
fi

if [ ! -f "$ALLOWED_CHATS_FILE" ] && [ ! -f "$ALLOWED_USERS_FILE" ]; then
    echo "$ALLOWED_CHATS_FILE and $ALLOWED_USERS_FILE file do not exist, you need at least one IDs file"
    exit 1
fi

# Read telegram token from file
TELEGRAM_TOKEN=$(cat "$TELEGRAM_TOKEN_FILE")

# Read chat IDs from file
if [ -f "$ALLOWED_CHATS_FILE" ]; then
    CHAT_IDS=$(cat "$ALLOWED_CHATS_FILE")
fi

# Read user IDs from file
if [ -f "$ALLOWED_USERS_FILE" ]; then
    USER_IDS=$(cat "$ALLOWED_USERS_FILE")
fi

# Merge chat IDs and user IDs into recipients array
RECIPIENTS=$(echo "$CHAT_IDS $USER_IDS" | tr ' ' '\n')

text=$(urlencode "$1")
for id in $RECIPIENTS; do
    curl "https://api.telegram.org/bot$TELEGRAM_TOKEN/sendMessage?chat_id=$id&text=$text"
done

exit 0

O primeiro é comprobar que teñamos argumentos, unha mensaxe a envíar. Se non, saímos de devolvemos un erro informando.

O seguinte define unha función para codificar a mensaxe. Ao enviala pola url non pode ir tal como chega, senon que temos que codificala a UTF-8.
No bucle iremos percorrendo cada caracter da mensaxe, para que no caso de non ser un caracter alfa-numérico, un punto, un guión baixo ou un guión, sustituirémolo polo seu equivalente, hexadecimal.

Continuamos por comprobar que os ficheiros cos IDs dos chats, cos destinatarios e o ficheiro co token de telegram existen. Se non, informamos e saímos cun erro. Se todo vai ben, lemos os contidos dos ficheiros. No caso dos IDs, creamos un único array cos IDs de usuario e de chat.
Xa no bucle, facemos con curl a chamada á API de telegram, para enviar a mensaxe previamente transformada a UTF-8.

Cos dous scripts en conxunto, cada vez que a temperatura esté por riba dos setenta grados centígrados, no momento de execución os usuarios e os chats indicados, recibirán unha mensaxe informando da temperatura.

Agora mesmo só é informativo, xa que non admite accións, pero a idea é engadir outro script, que no caso de ler unha orde determinada permita apagar a Raspberry remotamente.

Arquivos de configuración

Os arquivos de configuración están no cartafol config, dentro de bash. Son arquivos planos con extensión .dist. Están tan só a modo de exemplo. A idea é que, cando alguén, clone o proxecto copie estes arquivos sen a extensión. Sustituíndo a continuación o contido polos IDs dos usuarios ou chats cos que interacturar. No caso do ficheiro para o token, tamén, o baleiraremos e deixaremos só o noso token.

Deste xeito non gardaremos esa información sensible no repositorio. E cada quen pode por os seus propios IDs sen expolos. Eses ficheiros están debidamente excluídos no .gitignore.

run.sh

Para que sexa máis cómodo levantar o contenedor, engadín un pequeno script máis. Que entrará na carpeta do Dockerfile, construirá a imaxe e a lanzará.

cd docker
docker build -t cron_container .
docker run -d cron_container

Tokens e IDs

Cos scripts temos o esqueleto, pero faltaría o corazón. Por así dicilo. Xa que non podemos funcionar sen o token, ou os IDs.

O primeiro que precisamos é o token. Para obter un token, temos que ir a telegram e abrir un chat co BotFather. Este será o encargado de guiarnos nos pasos para crear o bot así como de configuralo.

Para crear un bot e obter o token, precisamos enviar a orde para isto. Neste caso /newbot. A continuación BotFather, comezará a facernos algunhas preguntas para a configuración inicial. O nome que queremos que apareza no chat e un nome de usuario para o bot. A restricción en canto ao nome de usuario é que non se pode repetir, e ten que rematar en bot.

Unha vez feito isto, obterás o token. Gárdao e non o compartas, xa que calquera que teña o token, poderá manexar a configuración do mesmo, ler as mensaxes, etc.

Agora se queremos cambiar algo da configuración, tamén se fará dende aquí. Por exemplo, se queremos que o noso bot poida ler conversacións dos grupos, podemos mandar o comando /setprivacy. BotFather nos preguntará a que bot dos que temos lla queremos mudar. Por defecto está como ENABLED, o que non permite ler mensaxes nos grupos. O bot tan só poderá ler aquelas mensaxes con comandos ou que se dirixan a el co @.

Unha vez teñamos o token e o noso bot creado, quedaría obter os IDs. Un xeito doado é engadir o bot ao grupo co que queiramos interactuar, ou abrir un chat co bot directamente co usuario do que queremos o ID.

Unha vez feito isto, escribimos calquer cousa e accedemos a seguinte URL https://api.telegram.org/bot<noso-token>/getUpdates. Isto nos devolverá un JSON no que poderemos ver o ID do usuario que envía a mensaxe e, no caso de facelo nun grupo, o ID do grupo. Estes serían os IDs a meter nos arquivos. Realmente poderían ir os IDs nun único arquivo, pero por organización prefiro separar usuarios de grupos. Cousas distintas, arquivos distintos.

Conclusión

Neste artigo vimos como crear un contenedor co Dockerfile, sen necesidade de utilizar docker-compose. Tamén vimos como instalar dende o repositorio de Alpine e copiar arquivos, do noso equipo ao contenedor.
A continuación, presentamos unha pequena introducción a Bash, con dous sinxelos scripts que nos permitirán enviar un aviso mediante telegram cando a temperatura exceda os setenta graos centígrados. Tamén explicamos, a grandes rasgos, como crear o noso bot, obter o token e configuralo, permitíndonos isto obter os datos cos IDs cos que queremos interactuar.

Aínda que usar un contenedor Docker para algo tan simple non é a solución máis eficiente, considero que é unha boa forma de evitar instalar cousas directamente no sistema o que podería emporcalo co tempo. Se nalgún momento atopo outro xeito de controlar a temperatura, só precisarei tumbar o contenedor e eliminalo sen preocuparme das suas dependencias.

Ademáis a idea é que este contenedor vaia medrando con novos scripts. Tal vez para controlar outras cousas do sistema, ou para administrar a Raspberry a través de Telegram. Permitindo, por exemplo, levantar ou tumbar servizos cunha mensaxe.

Pero isto quedará para máis adiante, xa que aínda non o teño claro. Pois tamén quero facer unha serie de bots con Python e pode que algunha linguaxe máis.

Así que de momento ata aquí este artigo.

Referencias

O código do script, pode atoparse no meu repo: https://codeberg.org/codigomorrazo/bash-scripts

Máis información da API de Telegram: https://core.telegram.org/api