Postagem em destaque

Criando bot do Telegram em Shell script com ShellBot

SHELLBOT

No dia 10/03/2017 realizei uma postagem divulgando um pequeno projeto chamado ShellBot, uma API desenvolvida em shell script para a criação de bots na plataforma Telegram. Desde então ela vem ganhando notoriedade e por consequência sua utilização vem crescendo exponencialmente. Após essa data novas atualizações foram disponibilizadas e algumas modificações realizadas para melhor atender os requisitos de desempenho e estabilidade do script.

Uma atualização ocorrida na versão 3.8 adicionava o suporte a criação de Threads, melhorando o desempenho no tratamento das requisições, em contra partida impactava o usuário no quesito usabilidade e inviabilizava o código modelo postado anteriormente. Devido a isso estou realizando uma atualização na postagem afim de abordar com maiores detalhes os tópicos relevantes do projeto.

Informo que a API continuará à receber atualizações sem impactar a experiência do usuário.

1. Criando o bot no Telegram

Antes de iniciar o download da API é necessário criar o seu bot utilizando o bot oficial do Telegram clicando aqui @BotFather.

Clique no botão INICIAR e faça os procedimentos a seguir:

1. Envie o comando /newbot
2. Envie o nome do seu bot.
3. Se o nome informado for válido, será solicitado o username.
4. Se o procedimento tiver êxito será retornado o número do token com o qual faremos a comunicação.

2. Baixando o SHELLBOT.

O projeto está disponível no Github oficial do blog.

Clonando o repositório do git.

$ git clone https://github.com/shellscriptx/ShellBot.git && cd ShellBot

3. Adicionando ao seu projeto.

Copie o arquivo ShellBot.sh para a pasta do seu projeto.

$ cp ShellBot.sh ~/pasta_projeto

4. Importando API

Para utilizar os métodos da api é necessário importar o source em seu script utilizando a seguinte linha de comando no inicio do código.

#!/bin/bash
# 
# INICIO
# bot_script

source ShellBot.sh
Nota: O procedimento de importação não requer permissão de execução ao ShellBot.

5. Inicializando o bot

A inicialização é altamente necessária a para comunicação entre seu código e o servidor do Telegram e é realiza por meio do método ShellBot.init com a sintaxe a seguir:

ShellBot.init --token <num_token>

Onde num_token é o número de identificação exclusivo do bot obtido durante a etapa 1.

Considere o código abaixo com um token hipotético.

# Importando API
#
# INICIO
# bot_script

# Importando API
source ShellBot.sh

# Token do bot
bot_token='123456789:dAe0QdEXXMoEd8pEBympiBQ0iNNQOx1YPix'

# Inicializando o bot
ShellBot.init --token "$bot_token"

# Imprime o username do bot.
ShellBot.username
Nota: A linguagem shell script é case sensitive, ou seja, faz diferenciação entre letras maiúsculas e minusculas, por este motivo fique atento a nomenclatura dos métodos. 

Salve as alterações e execute o script. Se tudo estiver correto será impresso na saída padrão o username do bot, caso contrário uma mensagem de erro será apresentada.

Saída:

$ ./bot_script
grupox_bot       # Nome do usuário do bot.

6. Obtendo atualizações

Todas as requisições feitas ao bot são armazenas em uma lista de objetos que contém as atualizações e que podem ser obtidas através do método ShellBot.getUpdates.

Sintaxe:

ShellBot.getUpdates --limit <max> --offset <update_id> --timeout <seconds>

limit - Limita o número de atualizações a serem recuperadas. Valores entre 1-100 são aceitos. Valor padrão é 100. (Opcional)

offset - Identificador da primeira atualização a ser retornada. Deve ser maior que os identificadores de atualizações recebidas anteriormente. Por padrão, as atualizações que começam com a atualização mais antiga não confirmada são retornadas. Uma atualização é considerada confirmada assim que o ShellBot.getUpdates é chamado com um deslocamento maior do que o update_id. O deslocamento negativo pode ser especificado para recuperar as atualizações a partir da atualização offset do final da fila de atualizações. Todas as atualizações anteriores serão esquecidas. (Opcional)

timeout - Tempo limite em segundos em espera por atualizações. O padrão é 0, ou seja, o acerto curto habitual. Deve ser positivo, a consulta curta deve ser usada somente para fins de teste. (Opcional)

Para obter constantemente as atualizações é necessário que o método seja executado continuamente e para isso é preciso inseri-lo em um loop infinito.

# Importando API
#
# INICIO
# bot_script

# Importando API
source ShellBot.sh

# Token do bot
bot_token='123456789:dAe0QdEXXMoEd8pEBympiBQ0iNNQOx1YPix'

# Inicializando o bot
ShellBot.init --token "$bot_token"

# Imprime o username do bot.
ShellBot.username

# loop
while :
do
    # Obtendo atualizações
    ShellBot.getUpdates --limit 100 --offset $(ShellBot.OffsetNext) --timeout 30
done
Nota: A função ShellBot.OffsetNext retorna o id da última atualização + 1, fazendo com que seja lido o próximo elemento da lista.

7. Tratando as requisições

A api possui uma gama de variáveis reservadas onde são armazenados os valores do objetos obtidos na chamada do método ShellBot.getUpdates. Para mais informações consulte a documentação na wiki.

As variáveis são declaradas como sendo do tipo array indexado, ou seja, uma estrutura de dados que armazena uma coleção de elementos de tal forma que cada um dos elementos possa ser identificado pelo seu índice e que são listados utilizando a função ShellBot.ListUpdates.

O código baixo demonstra a criação de um comando chamado /ajuda e associa um evento ao mesmo.

# Importando API
#
# INICIO
# bot_script

# Importando API
source ShellBot.sh

# Token do bot
bot_token='123456789:dAe0QdEXXMoEd8pEBympiBQ0iNNQOx1YPix'

# Inicializando o bot
ShellBot.init --token "$bot_token"

# Imprime o username do bot.
ShellBot.username

# loop
while :
do
    # Obtendo atualizações
    ShellBot.getUpdates --limit 100 --offset $(ShellBot.OffsetNext) --timeout 30

    # Lista os índices das atualizações.
    for id in $(ShellBot.ListUpdates)
    do
    # Bloco de instruções
    (
        # Verifica se a mensagem é do tipo comando.
        if [[ ${message_entities_type[$id]} == bot_command ]]
        then
            # Remove o sufixo contendo o username do bot e o '@' inclusive, extraindo somente o comando.
            case ${message_text[$id]%%@*} in
                '/ajuda')   
                    # Envia mensagem ao remetente.
                    ShellBot.sendMessage --chat_id ${message_chat_id[$id]} \
                                         --text "Olá *${message_from_first_name[$id]}*, em que posso ajudar?" \
                                         --parse_mode markdown
                ;;
            esac
         fi
    ) & # Thread
    done
done
Nota: O operador & cria uma thread que executa os comandos contidos dentro do bloco (...) em segundo plano e lê o próximo elemento da lista, tratando as requisições simultaneamente. Remova o operador '&' se deseja tratá-las em fila (uma por vez).

Observe que os valores das variáveis são acessados pelo índice do elemento armazenado em id. A função ShellBot.ListUpdates retorna todos os índices disponíveis e a cada interação o valor de id é atualizado para o próximo elemento.

Enviando o comando /ajuda para visualizar o resultado.


Note que ambos os comandos foram aceitos, pois o uso da expansão ${message...%%@*} remove o sufixo @grupox_bot e avalia somente o expressão /ajuda.

Função de boas-vindas

Acredito que para qualquer grupo essa seja a função fundamental de um bot, enviar uma mensagem de boas-vindas aos novos membros, mostrando que sua presença não passou desapercebida e até mesmo apresentar informações prévias sobre o tema. Vou implementar o código anterior com essa função.

# Importando API
#
# INICIO
# bot_script

# Importando API
source ShellBot.sh

# Token do bot
bot_token='123456789:dAe0QdEXXMoEd8pEBympiBQ0iNNQOx1YPix'

# Inicializando o bot
ShellBot.init --token "$bot_token"

# Imprime o username do bot.
ShellBot.username

# Função para envio da mensagem de boas-vindas
msg_bem_vindo()
{
    local msg

    # Texto da mensagem
    msg="🆔 [@${message_new_chat_member_username[$id]:-null}]\n"
    msg+="🗣 Olá *${message_new_chat_member_first_name[$id]}*"'!!\n\n'
    msg+="Seja bem-vindo(a) ao *${message_chat_title[$id]}*.\n\n"
    msg+='`Se precisar de ajuda ou informações sobre meus comandos, é só me chamar no privado.`'"[@$(ShellBot.username)]"

    # Envia a mensagem de boas vindas.
    ShellBot.sendMessage --chat_id ${message_chat_id[$id]} \
                            --text "$(echo -e $msg)" \
                            --parse_mode markdown

    return 0
}

# loop
while :
do
    # Obtendo atualizações
    ShellBot.getUpdates --limit 100 --offset $(ShellBot.OffsetNext) --timeout 30

    # Lista os índices das atualizações.
    for id in $(ShellBot.ListUpdates)
    do
    # Bloco de instruções
    (
        # Chama a função 'msg_bem_vindo' se o valor de 'message_new_chat_member_id' não for nulo.
        [[ ${message_new_chat_member_id[$id]} ]] && msg_bem_vindo

        # Verifica se a mensagem é do tipo comando.
        if [[ ${message_entities_type[$id]} == bot_command ]]
        then
            # Remove o sufixo contendo o username do bot e o '@' inclusive, extraindo somente o comando.
            case ${message_text[$id]%%@*} in
                '/ajuda')   
                    # Envia mensagem ao remetente.
                    ShellBot.sendMessage --chat_id ${message_chat_id[$id]} \
                                         --text "Olá *${message_from_first_name[$id]}*, em que posso ajudar?" \
                                         --parse_mode markdown
                ;;
            esac
         fi
    ) & # Thread
    done
done
Nota: Todas as variáveis precisam ser referenciadas por indexação para que a mensagem seja tratada corretamente. Observe que a variável id (índice) é precedida pelo seu identificador.

Repare que na chamada no método ShellBot.sendMessage no parâmetro --text, executei o comando $(echo -e $msg) ao invés de só passar o nome da variável. Por que ? Porque o corpo da mensagem armazenado em $msg contém o caractere de formatação \n (nova linha) que precisa ser interpretado e para isso é necessário o seu uso. Caso contrário seria impresso de forma literal. Utilize-o sempre que precisar formatar o texto.

Adicionado um novo membro e visualizando o resultado.


Modo monitor

Todo o processo de carregamento dos objetos, leitura e atribuição de valores ocorre em modo silencioso, exibindo apenas na saída padrão o retorno dos métodos. O modo monitor quando ativado lista na saída padrão todas as variáveis inicializadas referentes a mensagem tratada, seja ela recebida no privado, grupo ou canal. Esse recurso oferece ao usuário uma visualização dinâmica de eventos, facilitando assim a aplicação de testes.

Para ativar utilize o parâmetro -m ou --monitor na chamada do método ShellBot.init.

ShellBot.init --token <seu_token> --monitor

Saída padrão após o envio de uma mensagem.

=================== MONITOR ===================
Data: sáb set 16 09:06:31 -03 2017
Script: meu_bot.sh
Bot (nome): ▪️❌▫️
Bot (usuario): beta_bot
Bot (id): 123456789
-----------------------------------------------
Mensagem: 1
-----------------------------------------------
update_id = '870716554'
message_date = '1505563591'
message_message_id = '29159'
message_text = '/comando_teste'
message_chat_all_members_are_administrators = 'false'
message_chat_id = '-1122334455'
message_chat_title = 'grupoteste'
message_chat_type = 'group'
message_entities_length = '14'
message_entities_offset = '0'
message_entities_type = 'bot_command'
message_from_first_name = 'SHAMAN'
message_from_id = '11443322'
message_from_is_bot = 'false'
message_from_language_code = 'pt-BR'
message_from_username = 'x_SHAMAN_x'
As variáveis são exibidas com seus respectivos valores e separadas por mensagem.

Limpando histórico de mensagens (flush)

Todas as mensagens recebidas ficam salvas em uma longa lista contendo todos os objetos do corpo da mensagem e que podem ser acessadas utilizando seu update_id. Quando o bot fica offline por muito tempo e se durante esse período forem realizadas inúmeras requisições ao mesmo, tais requisições são colocadas em fila aguardando um futuro tratamento ao retorno do serviço. Mas em alguns casos isso pode ser um problema, considerando o número de mensagens contidas na fila e possivelmente gerando um retorno excessivo de informações na linha do tempo.

O recurso flush descarta todas as mensagens recebidas desde sua inatividade até o presente momento.  Para ativar o recurso utilize o parâmetro -f ou --flush na chamada do método ShellBot.init.

ShellBot.init --token <seu_token> --flush

Veja o retorno do método ShellBot.init após a restauração do serviço.

123456789|grupox_bot|▪️❌▫️|870716600|870716628
Os dois últimos campos contém o flush_first_id e o flush_last_id que representam um intervalo de ids que foram ignorados. Em nosso caso especifico foi um total de 28 mensagens.

Monitorando serviços do sistema.

Um cenário bastante interessante para essa implementação seria na auditoria e monitoração de um servidor. Empenhando a função de notificar a disponibilidade de um determinado serviço ou serviços ao administrador do sistema.

O exemplo abaixo ilustra uma simples aplicação que monitora o serviço ssh e notifica o usuário caso ocorra eventos stop ou start no serviço.

#!/bin/bash

# script: Monitor.sh
#
# Para melhor compreensão foram utilizados parâmetros longos nas funções; Podendo
# ser substituidos pelos parâmetros curtos respectivos.

# Importando API
source ShellBot.sh

# Token do bot
bot_token='123456789:dAe0QdEXXMoEd8pEBympiBQ0iNNQOx1YPix'

# Inicializando o bot
ShellBot.init --token "$bot_token"
ShellBot.username

# Serviço
service=sshd

# default
old=0

# Monitorar
while :
do
    # Testa o serviço e salva a saída em 'output'.
    output=$(systemctl -q status $service)

    # Atualiza o status.
    err=$?

    # Se o serviço for interrompido ou restaurado, envia uma mensagem notificando o usuário.
    (( ((err ^ old)) != 0 )) && ShellBot.sendMessage --chat_id 123456789 \
                                                        --text "$(echo -e "<b>Serviço: sshd</b>\n\n<b>Detalhes:</b>\n\n$output")" \
                                                        --parse_mode html

    # Salva o status anterior
    old=$err

    # delay
    sleep 2

done
Nota: Para esse tipo de implementação não é necessário a utilização do método ShellBot.getUpdates descrito no passo 6.

Interrompendo o serviço.

$ sudo systemctl stop sshd

Resultado:


Iniciando serviço.

$ sudo systemctl start sshd

Resultado:


Isso é apenas uma simples e humilde aplicação mas que nos fornece um deslumbre do que podemos criar. Quem sabe um bot autômato capaz de realizar intervenções pré-definidas ou até mesmo fornecer ao administrador opções de atuação para determinados tipos de problemas. Cabe a sua imaginação decidir que caminho tomar e como chegar até lá.

Espero que esse post seja útil para você e para mais informações não deixe de consultar a documentação do projeto na wiki clicando aqui.

Participe deixando seu comentário ou sugestão sobre o assunto.
Gostaria de apoiar o trabalho da comunidade? clique aqui

Comentários

  1. Respostas
    1. Vlw jovem, é nós. Tentando sempre criar alguma coisa. Kkkkkkk

      Excluir
  2. Valeu SHAMAN, ficou show de bola o texto, simples e direto!

    ResponderExcluir
    Respostas
    1. Obrigado. Espero que possa ser útil para a galera é quem sabe novos bots em Shell venham surgir.

      Excluir
  3. Parabéns pelo trabalho.
    Tentei fazer o clone com "git clone git@github.com:shellscriptx/ShellBot.git" e deu
    "Permission denied (publickey).
    fatal: Could not read from remote repository.
    "
    Com "git clone https://github.com/shellscriptx/ShellBot.git
    " deu certo.
    Valeu.


    git clone https://github.com/shellscriptx/ShellBot.git

    ResponderExcluir
    Respostas
    1. Obrigado pela observação e a correção já foi realizada na postagem.

      Excluir
  4. Olá,

    Acabei de testar o script e achei super prático, agora gostaria de saber se existe a possibilidade dele executar outro script. Ex : quando eu executar o comando /uptime ele retornar a informação do servidor ?

    ResponderExcluir

Postar um comentário

Contato

Nome

E-mail *

Mensagem *