Fazendo um Bot para Telegram em Python

Para quem não conhece, os Bots do Telegram são entidades que parecem usuários e têm funções específicas. Quando você conversa com um Bot ou coloca ele em um grupo, você pode dar comandos para que ele realize uma atividade (um jogo, por exemplo) ou te passe informações. Se você deseja experimentar algum Bot, eu recomendo o @mau_mau_bot. Basta adicionar o Bot a um grupo do Telegram e vocês vão poder jogar UNO 😀 E o melhor, também é feito em Python!

Sabendo o que é um Bot, vamos para o que interessa: como criar um? Para isso, eu proponho fazer um HelpDesk Bot. Basicamente, é um Bot que vai receber mensagens de um usuário e encaminhar para um outro usuário ou um grupo. Daí, quando alguém responder essas mensagens, a resposta será encaminhada ao usuário pelo Bot. Ou seja, ele vai ser um intermediário entre o usuário e o suporte.

PS: Todos os códigos a seguir estão em inglês, mas a explicação está em português.

1ª Etapa: BotFather

Para criarmos um Bot que se conecte ao Telegram, temos que conversar com o @BotFather. Ele é o responsável pela criação de novos bots. Para isso, converse no Telegram com ele e envie o seguinte comando:

/newbot – Para criar seu novo Bot. O BotFather vai te perguntar o nome dele e o usuário que ele deve receber. O usuário deve sempre terminar em “bot” e não pode ser um usuário existente.

Ao criar o novo bot, o BotFather irá te retornar um token. Nós precisaremos dele no futuro, mas não é preciso gravá-lo. Se perder o token, é só usar o comando /token que o BotFather vai te lembrar qual é.

2ª Etapa: Configurações

Para criar este Bot, utilizaremos o Redis para armazenar as configurações do usuário. No nosso caso, apenas a linguagem (português ou inglês). Para isso, instale o redis-server e o módulo redis para Python e vamos criar nosso arquivo de configurações.

apt-get install redis-server
pip install redis

O arquivo será um .ini, um formato padrão para este tipo de arquivo. Vou chamá-lo de config.ini e colocá-lo na raíz do projeto. Ele terá a seguinte estrutura:

[DEFAULT]
token=<insira seu token aqui>
support_chat_id=<insira seu id de usuário ou de grupo aqui>

[DB]
host=localhost
port=6379
db=0

Um arquivo .ini é organizado da seguinte forma: seções entre [ e ], que são equivalentes a categorias; chaves como se fossem variáveis, sendo a sintaxe chave=valor. Mesmo que o valor seja uma string, nunca irá entre aspas.

Na seção DEFAULT, iremos armazenar nosso token e um support_chat_id, que é onde devemos colocar o id do usuário ou do grupo que deve receber as mensagens de suporte que o bot vai encaminhar. Esse id não é o nome de usuário, é um número de identificação. Mais adiante vamos ver onde encontrá-lo.

Além disso, temos as informações do banco de dados na seção DB. Estas informações são o local onde nosso banco de dados pode ser acessado.

3ª Etapa: Conectando ao bot

Para nos conectarmos ao bot que criamos usando o BotFather iremos utilizar o módulo python-telegram-bot. Este módulo faz a conexão do nosso programa com a API de bots do Telegram.

Para instalar este módulo, usamos o pip:

pip install python-telegram-bot

E agora podemos começar nosso arquivo main.py, que é onde iremos nos conectar ao bot e já aplicar nossas configurações:

import telegram
import configparser
import redis

from telegram.ext import Updater

# Configuring bot
config = configparser.ConfigParser()
config.read_file(open('config.ini'))

# Connecting to Telegram API
# Updater retrieves information and dispatcher connects commands
updater = Updater(token=config['DEFAULT']['token'])
dispatcher = updater.dispatcher

# Connecting to Redis db
db = redis.StrictRedis(host=config['DB']['host'],
                       port=config['DB']['port'],
                       db=config['DB']['db'])

Primeiro, utilizamos o módulo configparser para pegar as informações do arquivo config.ini. Salvamos essas informações na variável config e agora podemos recuperar todas as informações como se viessem de um dicionário, no formato config[seção][chave].

Depois disso, criamos uma variável updater que é uma instância da classe Updater do módulo do Telegram. O que essa instância faz é ficar “vigiando” nosso bot e sempre que ele receber uma mensagem, é o updater que irá receber essa informação. Para isso, o updater deve receber o token do nosso bot.

Além do updater, configuramos o dispatcher. Se é o updater que verifica o recebimento de mensagens, o dispatcher é quem conecta essa mensagem ao bot e permite que o bot responda.

Por fim, configuramos o banco de dados, na variável db, como sendo uma instância do Redis com as informações que colocamos no arquivo de configuração.

A partir de agora, você pode executar o bot. Primeiro, execute em uma instância da linha de comando o redis-server e deixe-o rodando lá. Depois, execute em outra instância uma shell Python. Nessa shell, faça:

from main import updater
updater.start_polling()

Pronto. Agora seu bot já pode responder pra você no Telegram 🙂

PS: Sempre que você fizer uma modificação no seu bot, precisará fechar a conexão com a shell do Python e começá-la novamente.

3ª Etapa e Meia: Verificando erros

A partir do momento que seu Bot está conversando por meio do updater.start_polling(), você não será capaz de ver nenhum erro do seu código Python. Quando acontecer um erro, o Bot só não vai te responder ou fazer aquilo que ele deveria. Por isso, é importante você usar um logging no seu código ou alguma ferramenta de debug para vigiar seus erros. Mas, caso queira uma solução simples, você pode fazer o seguinte quando estiver acontecendo um erro:

  1. Faça alguns prints() e verifique até onde eles ocorrem e quando deixam de ocorrer, para verificar onde está o erro.
  2. Quando identificar o erro, coloque alguns:
try:
    o código dando erro
except Exception as e:
    print(e)

Para saber qual erro está acontecendo. Feito isso, agora é só tentar resolvê-lo 😉

4ª Etapa: Recebendo comandos

Nosso bot vai funcionar por comandos que o usuário passar para ele. Por exemplo, se o usuário enviar /support queremos que o Bot receba uma mensagem de suporte e se o usuário enviar /settings queremos que ele exiba as configurações.

Para isso, vamos adicionar no main.py:

def start(bot, update):
    """
        Shows an welcome message and help info about the available commands.
    """
    me = bot.get_me()

    # Welcome message
    msg = "Hello!\n"
    msg += "I'm {0} and I came here to help you.\n".format(me.first_name)
    msg += "What would you like to do?\n\n"
    msg += "/support - Opens a new support ticket\n"
    msg += "/settings - Settings of your account\n\n"

    # Commands menu
    main_menu_keyboard = [[telegram.KeyboardButton('/support')],
                          [telegram.KeyboardButton('/settings')]]
    reply_kb_markup = telegram.ReplyKeyboardMarkup(main_menu_keyboard,
                                                   resize_keyboard=True,
                                                   one_time_keyboard=True)

    # Send the message with menu
    bot.send_message(chat_id=update.message.chat_id,
                     text=msg,
                     reply_markup=reply_kb_markup)

Linha 1: A função start() será nosso primeiro comando. Quando o usuário enviar para o Bot /start, é essa função que deve ficar responsável por promover a resposta. Para isso, a função recebe dois parâmetros: bot e update. O parâmetro bot é uma instância do próprio bot e o parâmetro update é um conjunto de informações sobre a mensagem recebida pelo usuário.

Linhas 5 – 12: Depois, salvamos na variável me os dados do nosso bot a partir da função get_me() e, em seguida, criamos a mensagem que iremos enviar para o usuário. Nessa mensagem, inserimos o nome do bot a partir da variável me.

Linhas 15-16: Também iremos criar um teclado personalizado. Esse teclado vai fornecer para o usuário as opções de comandos possíveis para que ele não tenha que digitá-los. Um teclado personalizado é uma lista de listas de valores do tipo KeyboardButton. Ou seja, uma lista é o teclado e cada outra lista interna é uma linha de botões (sim, podemos ter mais de um botão por lista/linha).

O botão irá receber uma mensagem de texto que irá aparecer para o usuário. Quando o usuário clicar nesse botão, o Telegram vai fazer com que o usuário envie uma mensagem com o conteúdo do botão. Isto é, se o usuário clica em um botão “/support” será como se ele estivesse enviando uma mensagem com o comando “/support“.

Linhas 17-19: Depois de criar nosso teclado personalizado, adicionamos ele a um Markup, que é como um “layout”. Esse markup recebe nosso teclado e as opções de resize_keyboard, caso o Telegram possa redimensioná-lo, e one_time_keyboard, caso o teclado deva desaparecer depois que o usuário clicar em uma opção.

Linhas 22-24: Por fim, chamamos o método send_message() do bot para fazer com que ele envie uma mensagem para o usuário. A mensagem deve receber o chat_id, que é o id para o qual ele deve responder e que pode ser conseguido pela própria mensagem recebida do usuário por meio de update.message.chat_id; um text, que é a mensagem a ser enviada; e pode ou não receber um reply_markup, que é um layout personalizado (no nosso caso, o do teclado).

Agora, podemos adicionar o seguinte ao arquivo main.py:

from telegram.ext import Updater, CommandHandler
...
start_handler = CommandHandler('start', start)
dispatcher.add_handler(start_handler)

CommandHandler é a classe responsável por ligar um comando a uma função Python. Nesse caso, quando o comando for /start será chamada a função start(). Por fim, usamos o dispatcher para ligar esse handler ao nosso bot do Telegram.

Podemos repetir o processo para outros comandos:

def support(bot, update):
    """
        Sends the support message. Some kind of "How can I help you?".
    """
    bot.send_message(chat_id=update.message.chat_id,
                     text="Please, tell me what you need support with :)")


support_handler = CommandHandler('support', support)
dispatcher.add_handler(support_handler)

E, inclusive, ter uma resposta padrão quando o comando não for reconhecido:

from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
...
def unknown(bot, update):
    """
        Placeholder command when the user sends an unknown command.
    """
    msg = "Sorry, I don't know what you're asking for."
    bot.send_message(chat_id=update.message.chat_id,
                     text=msg)

unknown_handler = MessageHandler([Filters.command], unknown)
dispatcher.add_handler(unknown_handler)

5ª Etapa: Outros handlers

Existem dois outros handlers que vão nos ajudar: MessageHandler e RegexHandler. Por isso, já podemos importá-los lá no início. Agora, vamos ver como eles vão ser aplicados:

from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
...
def support_message(bot, update):
    """
        Receives a message from the user.

        If the message is a reply to the user, the bot speaks with the user
        sending the message content. If the message is a request from the user,
        the bot forwards the message to the support group.
    """
    if update.message.reply_to_message and \
       update.message.reply_to_message.forward_from:
        # If it is a reply to the user, the bot replies the user
        bot.send_message(chat_id=update.message.reply_to_message
                         .forward_from.id,
                         text=update.message.text)
    else:
        # If it is a request from the user, the bot forwards the message
        # to the group
        bot.forward_message(chat_id=int(config['DEFAULT']['support_chat_id']),
                            from_chat_id=update.message.chat_id,
                            message_id=update.message.message_id)
        bot.send_message(chat_id=update.message.chat_id,
                         text="Give me some time to think. Soon I will return to you with an answer.")

support_msg_handler = MessageHandler([Filters.text], support_message)
# Message handler must be the last one
dispatcher.add_handler(support_msg_handler)

Nesse caso, temos um MessageHandler. Isto quer dizer que, sempre que recebermos uma mensagem de texto (já que estamos usando o filtro de texto) que não se encaixa nos outros handlers, ela será tratada por este como uma mensagem comum do usuário. Por isso, este handler deve ser o último a ser adicionado, pois assim ele só será chamado quando nenhum outro o for.

Linha 11: Ok. Recebemos uma mensagem do usuário e ela foi passada para esta função. O que estamos fazendo? Primeiro, verificamos se a mensagem é uma resposta a uma mensagem que foi encaminhada ou se ela é só uma mensagem comum. Se for uma resposta, significa que é alguém respondendo a um ticket do usuário. Se não for, é o usuário enviando um ticket.

Linhas 14-16: Se for um usuário respondendo o ticket, fazemos o bot enviar uma mensagem para o usuário com a resposta.

Linhas 17-24: Se for o usuário abrindo o ticket, iremos encaminhar a mensagem do usuário para o responsável pelo bot usando o método forward_message(). Neste caso, encaminhamos para aquele chat_id que configuramos. Atenção: Se você ainda não achou seu chat_id para colocá-lo nas configuraõçes, é só printar o seu update.message.chat_id numa conversa com o bot. No caso de um usuário só, o chat_id é o id do próprio usuário.

 

Agora, outro tipo de handler será aplicado em:

from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, RegexHandler
...
def settings(bot, update):
    """
        Configure the messages language using a custom keyboard.
    """
    # Languages message
    msg = "Please, choose a language:\n"
    msg += "en_US - English (US)\n"
    msg += "pt_BR - Português (Brasil)\n"

    # Languages menu
    languages_keyboard = [
        [telegram.KeyboardButton('en_US - English (US)')],
        [telegram.KeyboardButton('pt_BR - Português (Brasil)')]
    ]
    reply_kb_markup = telegram.ReplyKeyboardMarkup(languages_keyboard,
                                                   resize_keyboard=True,
                                                   one_time_keyboard=True)

    # Sends message with languages menu
    bot.send_message(chat_id=update.message.chat_id,
                     text=msg,
                     reply_markup=reply_kb_markup)


def kb_settings_select(bot, update, groups):
    """
        Updates the user's language based on it's choice.
    """
    chat_id = update.message.chat_id
    language = groups[0]

    # Available languages
    languages = {"pt_BR": "Português (Brasil)",
                 "en_US": "English (US)"}

    # If the language choice matches the expression AND is a valid choice
    if language in languages.keys():
        # Sets the user's language
        db.set(str(chat_id), language)
        bot.send_message(chat_id=chat_id,
                         text="Language updated to {0}"
                         .format(languages[language]))
    else:
        # If it is not a valid choice, sends an warning
        bot.send_message(chat_id=chat_id,
                         text="Unknown language! :(")

settings_handler = CommandHandler('settings', settings)
get_language_handler = RegexHandler('^([a-z]{2}_[A-Z]{2}) - .*',
                                    kb_settings_select,
                                    pass_groups=True)

dispatcher.add_handler(settings_handler)
dispatcher.add_handler(get_language_handler)

Bom, na primeira função settings() não fizemos nada que não vimos até agora, então vamos focar em kb_settings_select(). Esta função está ligada a um RegexHandler. Este handler verifica se a mensagem segue um padrão de regular expression. Neste caso, estou verificando se a mensagem segue o seguinte padrão:

2 letras minusculas_2 letras maiusculas - qualquer coisa

Por exemplo: pt_BR – Português (Brasil)

Mas por que estou verificando isso? Porque quando o usuário selecionar uma linguagem no teclado das configurações, as opções são todas formatadas desse jeito. Ou seja, toda configuração de linguagem segue esse modelo.

Linha 27: Ok, então meu regex passou e agora chamamos a função kb_settings_select() para tratar essa mensagem que o usuário mandou, que é a opção de linguagem dele. Essa função recebe um parâmetro groups pois a opção pass_groups no meu handler está ativa. No regex, um group é tudo aquilo que está contido entre ( e ). No caso desse meu regex, se você observar a forma como ele está formatado no handler, o group das minhas mensagens vai ser o código inicial da mensagem: pt_BR, en_US, etc.

Linhas 32-39: Como groups será uma lista de groups do regex, pegamos apenas o índice 0, que é onde está a minha linguagem escolhida. Daí, verificamos se a linguagem escolhida é uma linguagem válida, e não uma falsificada que passaria no nosso regex (por exemplo, it_IT – Italiano passaria no regex, mas não é uma linguagem válida pois vamos trabalhar apenas com pt_BR e en_US).

Linhas 40-48: Se a linguagem é válida, armazenamos no nosso banco de dados a configuração do usuário com o comando db.set(chave, valor) e, se não for, falamos que a linguagem é desconhecida. Nossa chave do banco de dados é o id do usuário ou do grupo, e o valor é a linguagem escolhida.

6ª Etapa: Realizando traduções

Já armazenamos a escolha de linguagem do nosso usuário no banco de dados, mas ainda não estamos traduzindo as mensagens. Como faremos isso? Iremos utilizar o módulo gettext para pegar todas as mensagens do programa que queremos traduzir e depois iremos utilizar um decorator.

Adicione o seguinte no main.py:

import gettext
# Config the translations
lang_pt = gettext.translation("pt_BR", localedir="locale", languages=["pt_BR"])
def _(msg): return msg

Nossas mensagens a serem traduzidas vão ser sinalizadas por uma função _(). Por isso, adicione _() em volta de todas as strings que você deseja traduzir. Assim:

msg = _("Hello!\n")

As traduções funcionam assim: vamos ter um arquivo cheio de linhas no formato “msg original -> msg traduzida” que será o nosso arquivo de traduções. Esse arquivo deve ficar armazenado num diretório de traduções que tem a seguinte estrutura:

pasta_traduções/linguagem/LC_MESSAGES/arquivo_de_tradução

Linha 4: Neste caso, o método translations() que utilizamos vai ter que: nosso localedir é pasta_traduções, que é a pasta onde estão nossas traduções; languages é linguagem, o nome do diretório especificando a linguagem para a qual estou traduzindo; domínio (1º argumento) é arquivo_de_tradução, o nome do arquivo contendo as traduções sem a extensão do arquivo.

No caso da tradução deste programa, devemos ter um diretório com a seguinte estrutura:

locale/pt_BR/LC_MESSAGES/pt_BR.po

Esse arquivo .po não é gerado pelo Python. Para gerá-lo, recomendo que você use o poedit na função de pegar as mensagens de um arquivo fonte. Neste caso, é só indicar que o arquivo é o main.py e todas as mensagens para a tradução estão dento da função _(). Assim, ele irá gerar automaticamente para você o arquivo com todas as mensagens e você ainda pode traduzi-las por meio dele mesmo 🙂

Feito isso, o poedit irá gerar arquivos .mo, que são nossas traduções “compiladas”. Agora, é só aplicar essas traduções no programa, criando o seguinte decorator:

from functools import wraps
...
def user_language(func):
    @wraps(func)
    def wrapped(bot, update, *args, **kwargs):
        lang = db.get(str(update.message.chat_id))

        global _

        if lang == b"pt_BR":
            # If language is pt_BR, translates
            _ = lang_pt.gettext
        else:
            # If not, leaves as en_US
            def _(msg): return msg

        result = func(bot, update, *args, **kwargs)
        return result
    return wrapped

Linhas 6-15: O que este decorator está fazendo? Primeiro, ele pega a configuração de linguagem do usuário no banco de dados e salva ela na variável lang. Se a opção for pt_BR, ele muda a função _() para que ela receba o método gettext() da nossa translation() para pt_BR. Se lang é None ou en_US, ele faz com que _() retorne a mensagem original, sem traduzir (pois todas as nossas mensagens estão em inglês). O restante é sintaxe comum de decorators, então se tem alguma dúvida pesquise sobre eles, ok?

Pronto, agora basta aplicarmos esse decorator em todos os nossos comandos para que nossas mensagens sejam traduzidas ou não de acordo com a configuração do usuário. Desse jeito:

@user_language
def start(bot, update):
    """
        Shows an welcome message and help info about the available commands.
    """
    me = bot.get_me()
    ...

Feito!

Pronto, já temos um bot do Telegram completo. Você pode testá-lo com qualquer comando que configuramos e ver as respostas desse bot. Como o bot ainda não está hospedado, ele só responderá enquanto você estiver executando o start_polling() no seu computador. Agora, se você quer torná-lo público para o mundo, escolha um host Python e deixe esse script rodando lá para a eternidade 😀

O código do bot completo pode ser visto em: https://github.com/juliarizza/helpdeskbot

Parabéns! 😉

Anúncios

12 comentários sobre “Fazendo um Bot para Telegram em Python

  1. Angelo Sampaio disse:

    Achei bastante didático e interessante. Ao executar o código encontro o erro:

    File “main.py”, line 20, in
    lang_pt = gettext.translation(“pt_BR”, localedir=”locale”, languages=[“pt_BR”])
    File “/usr/lib/python2.7/gettext.py”, line 469, in translation
    raise IOError(ENOENT, ‘No translation file found for domain’, domain)
    IOError: [Errno 2] No translation file found for domain: ‘pt_BR’

    Se eu comento a linha 20 ( justamente aquela que configura a tradução) o bot funciona. Como corrigir este problema?

    Curtir

    • Júlia Rizza disse:

      Esse erro acontece porquê você tem que criar os arquivos de tradução **manualmente**, já que você é quem precisa falar o que cada string significa na língua que está traduzindo. Se você não quiser trabalhar com tradução, é só remover todas as partes do código que tratam disso. Se quiser, precisa gerar os arquivos .mo com a tradução. Quando comentei sobre as traduções lá no texto eu expliquei o passo a passo de como fazer isso 🙂

      Se você tiver feito tudo para gerar as traduções e mesmo assim este erro está ocorrendo, é porque ele não está encontrando o arquivo .mo no diretório /locale/pt_BR/. Neste caso, verifique onde se encontra sua tradução.

      Curtir

  2. possebon disse:

    Júlia,

    Muito bom o seu post sobre o Bot no Telegram. Eu consegui implementar algo muito parecido, mas eu estou com uma dúvida.

    Entendo que o bot é um programa que vai ficar executando constantemente, em algum servidor, à espera de algum comando.

    Essa parte está OK. Agora e se eu quiser fazer um outro programa, que envia mensagens para que esse bot possa processar, por exemplo, ocorre um evento qualquer, ao invés de enviar um e-mail, quero enviar uma mensagem para o bot, e conforme o conteúdo da mensagem, ele vai ter os CommandHandlers para atuar.

    Eu tentei fazer algo parecido, mas não está funcionando.

    Curtido por 1 pessoa

  3. José R Alves Junior disse:

    Achei super dahora, porém o meu código da este problema o erro e no arquivo util do telebot oque eu faco???
    2017-11-28 17:20:10,393 (util.py:64 WorkerThread1) ERROR – TeleBot: “AttributeError occurred, args=(“‘module’ object has no attribute ‘KeyboardButton'”,)
    Traceback (most recent call last):

    Curtir

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s