Como criar um Bot Telegram para atualizar uma planilha financeira no Google Sheets

Como criar um Bot Telegram para atualizar uma planilha financeira no Google Sheets

Nesse post nós mostramos como automatizar uma planilha financeira pessoal usando Python. Vimos como atualizar as transações a partir de um arquivo de extrato bancário em csv. Todavia, muitas transações no nosso dia a dia não são feitas por meio de um banco. Podem ser feitas em dinheiro, ou usando cartão de crédito. E seja numa planilha como foi apresentado, ou utilizando um app de finanças pessoais, fazer esses lançamentos no momento que ocorrem pode demandar algum tempo, e se deixarmos para fazer posteriormente, isso é passível de esquecimento. E se pudéssemos fazer isso por meio do envio de uma mensagem instantânea? Neste artigo iremos mostrar como criar um bot Telegram usando Python, que recebe essa mensagem e faz o registro da transação na nossa planilha.

Fonte: https://br.freepik.com/

Pré-requisitos

  • Conta no Google e credenciais no Google Cloud
  • Python e bibliotecas instaladas
  • Bot Telegram
Já vimos num post anterior como criar credenciais no Google Cloud e também o código Python que faz essa conexão com nossa planilha. Já, neste outro artigo, mostramos como criar um Bot Telegram. Aqui, vamos utilizar o mesmo bot.
Instale as bibliotecas que serão necessárias:

pip install python-dotenv python-telegram-bot gspread google-auth

Iniciando a implementação

Um script para bot Telegram deve ficar executando e a determinado intervalo de tempo, checa se o usuário interagiu enviando algum comando. Se o comando enviado for válido, ou seja, estiver entre os comandos previstos no script, o bot irá responder enviando uma simples mensagem ou executando uma ação mais complexa. Neste script inicial, nosso bot, responde com uma mensagem de boas vindas ao comando /start.

finance_bot.py
from dotenv import load_dotenv
import os
from telegram import Update
from telegram.ext import (
    ApplicationBuilder,
    CommandHandler,
    ContextTypes)
load_dotenv()

TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")


# Handlers
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    await update.message.reply_text(
        ("Seja bemvindo ao Finance Bot! "
         "Envie /help para ver os comandos disponíveis.")
    )

# Os demais handlers serão inseridos aqui

def main():
    if not TELEGRAM_TOKEN:
        raise RuntimeError("Defina as variáveis de ambiente TELEGRAM_TOKEN.")

    app = ApplicationBuilder().token(TELEGRAM_TOKEN).build()

    app.add_handler(
        CommandHandler("start", start))
    app.run_polling()


if __name__ == "__main__":
    main()

A fim de implementar o script do bot iremos precisar do token que você anotou quando criou o seu bot seguindo os passos que foram descritos em post anterior. Recomendamos que este token e outros dados sensíveis sejam mantidos em um arquivo .env e sejam utilizados em nosso programa por meio da biblioteca dotenv, assim como explicamos nesse post.

Após essas definições iniciais, tais como os imports e a inicialização do token, partimos para a criação dos handlers, que é como chamamos as funções que devem ser executadas em resposta aos comandos.

O primeiro handler que implementamos é o start, que será executado quando enviarmos o comando /start. Não é necessário que o nome do handler seja exatamente o mesmo nome do comando. Mais adiante explicamos o porquê.

Na assinatura da função observe que temos uma palavra-chave async. Essa palavra-chave define que uma função é assíncrona, ou seja, que executa de modo concorrente, sem bloquear a thread principal. Geralmente as chamadas à API, acessos a banco de dados, entre outros, precisam desse cuidado, pois são tarefas que demandam tempo para responder.

Ainda no contexto da função start(), a chamada reply_text(), que envia uma mensagem ao usuário, é precedida de um await, que serve para pausar a execução da função até que esta chamada seja resolvida. Um await só pode ser utilizado no contexto de uma função async.

Depois dos handlers, temos a função main(), que efetivamente irá executar o bot. Ela que cria uma instância do bot, adiciona os handlers e executa o script.

Antes de qualquer coisa nós verificamos se há um token definido, depois criamos a instância do bot. Na linha seguinte adicionamos o handler que foi definido. Note que a classe CommandHandler tem dois parâmetros: uma string, que será o comando ao qual o bot irá responder, e um segundo parâmetro que é o nome da função handler. Daí o porquê de comando e função não precisarem ter o mesmo nome.

Finalmente, chamamos o método run_polling(), que inicia o bot e o mantém executando continuamente buscando atualizações do servidor Telegram. Esse método bloqueia a thread corrente até que o processo seja interrompido, teclando CTRL + C.

Execute o script digitando no terminal python finance_bot.py. Se tudo estiver certo procure o bot pelo nome no Telegram e envie /start e você deve receber a mensagem de boas vindas.

Maneiras de executar o Bot Telegram

Antes de prosseguirmos com o nosso código, precisamos falar das maneiras de manter um bot Telegram executando.

Chamar run_polling() no terminal é uma maneira de testar nosso código. Assim, é possível checar se o bot está ouvindo as mensagens e respondendo. O uso de polling é possível fazer em servidores como Render, Railway, entre outros, de uma maneira bem simples, mas tem o inconveniente de o bot sair do ar se o processo parar ou precisar reiniciar.

Uma maneira mais profissional consiste em utilizar um webhook. Esse sistema é utilizado para que dois sistemas se comuniquem, e um deles é comunicado quando um evento acontece, em vez de precisar ficar verificando periodicamente se esse evento aconteceu, tal como no polling.

Tem a vantagem de estar sempre disponível, mas precisa de um endpoint (criado com Flask ou FastAPI, por exemplo) e de um domínio HTTPS e certificado SSL. No próprio Render é possível hospedar um webhook. Já o Ngrok é um serviço que possibilita que o webhook fique rodando em nosso computador. De todo modo, assinar esses serviços no plano gratuito tem o inconveniente do serviço "dormir" devido a algum tempo de inatividade e o Telegram pode não esperar (devido o timeout) e nossa mensagem ao bot vai ficar sem resposta.

Este tema é um pouco avançado e não cabe no escopo deste artigo. Mas se houver interesse podemos apresentá-lo em um post no futuro.

Escrevendo os demais handlers

Os outros handlers do nosso bot deverão responder aos comandos /help, que exibe uma ajuda sobre os comandos disponíveis, /save, responsável por registrar um lançamento na planilha, e /last que vai exibir os últimos 5 lançamentos que foram feitos.

O código de tais handlers serão incluídos logo após o start(). Iniciamos mostrando o help_command(), o qual é semelhante ao start(), e não necessita maiores comentários.

async def help_command(
        update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    help_text = (
        "Comandos disponíveis:\n"
        "/start - Iniciar o bot\n"
        "/help - Mostrar esta mensagem de ajuda\n"
        "/save - Salvar um lançamento na planilha\n"
        "   /save valor/categoria/tipo/descrição (descrição é opcional)\n"
        "   Tipo deve ser 'Despesa' ou 'Receita'\n"
        "   Exemplo: /save 50,00/Alimentação/Despesa/Jantar com amigos\n"
        "/last - Mostrar as últimas 5 transações\n"
    )
    await update.message.reply_text(help_text)

Antes de comentarmos o próximo handler, save_command(), precisamos falar do módulo utils.py, que tem duas funções auxiliares. Primeiro, conectar_google_sheets(), responsável por fazer a conexão com a API do Google Sheets, a qual já utilizamos nos posts anteriores, e normalizar_string(), que converte strings para um formato específico, o qual possibilita totalizações e comparações na planilha.

utils.py
import gspread
import unicodedata
import os
from google.oauth2.service_account import Credentials


def conectar_google_sheets():
    scopes = [
        "https://www.googleapis.com/auth/spreadsheets",
        "https://www.googleapis.com/auth/drive",
    ]

    creds = Credentials.from_service_account_file(
        "credenciais.json", scopes=scopes)

    client = gspread.authorize(creds)
    return client


def normalizar_string(s: str) -> str:
    normalized = unicodedata.normalize('NFKD', s)
    folded = "".join([c for c in normalized if not unicodedata.combining(c)])
    return folded.casefold()

Este módulo necessita que gspread seja instalado e importado para lidar com planilhas Google. Veja este artigo para saber mais sobre se conectar à API Google Sheets.

A função normalizar_string() precisa do pacote unicodedata, nativo do Python. Ela é reponsável por substituir caracteres acentuados e cedilha, e converter uma string em caixa baixa.

# Acrescente estes imports
from datetime import date
from utils import (
    conectar_google_sheets,
    normalizar_string)

# Handler save
async def save_command(
        update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    # Verifica se o usuário forneceu argumentos
    if not context.args:
        await update.message.reply_text(
            ("Por favor, forneça informações a registrar separadas por /."
             "Digite no formato: valor/categoria/tipo/descrição."
             "Descrição é opcional."
             )
        )
        return
    # Obtém os dados fornecidos pelo usuário e separa-os
    dados = context.args[0].split("/")
    # Verifica se há pelo menos 3 partes (valor, categoria, tipo)
    if len(dados) <= 3:
        await update.message.reply_text(
            ("Formato inválido. Use: valor/categoria/tipo/descrição"
             )
        )
        return
    # Valida o valor fornecido
    valor = dados[0].replace(",", ".")
    try:
        float(valor)
    except ValueError:
        await update.message.reply_text(
            "O valor deve ser um número. Por favor, tente novamente."
        )
        return
    cliente = conectar_google_sheets()
    planilha = cliente.open("Minhas Finanças Pessoais")
    sheet = planilha.worksheet("Transações")
    # Adiciona uma nova linha na planilha com os dados fornecidos
    sheet.append_row(
        [
            date.today().strftime("%d/%m/%Y"),
            dados[3] if len(dados) > 3 else "",
            normalizar_string(dados[1]).capitalize(),
            float(valor),
            normalizar_string(dados[2]).capitalize(),
        ]
    )
    # Dá feedback ao usuário
    await update.message.reply_text(
        f"""
        Você registrou: {dados[0]} para {dados[1]}
        no dia {date.today().strftime('%d/%m/%Y')}.
        """
    )

O comando /save é o mais complexo do nosso bot, até o momento. O comando /help diz como deve ser o formato de /save e dá um exemplo: /save 50,00/Alimentação/Despesa/Jantar com amigos. Ou seja, /save tem argumentos, que são os dados a serem registrados na planilha. A fim de assegurar um mínimo de estrutura nos dados, são necessárias algumas validações, as quais são tratadas pelo handler.

Assim, o primeiro passo é verificar se foram digitados os arguntos após o /save. Depois é verificado se existem pelo menos três informações nos argumentos, visto que Descrição é opcional. Em seguida tratamos o valor numérico da transação, que deve ser um float.

Após tudo validado, a função faz a conexão com a API, e faz um apend_row() desses dados na planilha. Feito isso, o bot nos dá um feedback, avisando que o registro foi feito.

Finalmente incluimos o handler print_last_transactions(), o qual irá retornar os cinco últimos registros feitos na planilha.

async def print_last_transactions(
        update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    cliente = conectar_google_sheets()
    planilha = cliente.open("Minhas Finanças Pessoais")
    sheet = planilha.worksheet("Transações")
    registros = sheet.get_all_records()
    ultimos = registros[-5:]  # Últimas 5 transações
    mensagem = "Últimas 5 transações:\n"
    for registro in ultimos:
        mensagem += (
            f"{registro['Data']}: {registro['Tipo']} de "
            f"{registro['Valor']} em {registro['Categoria']} - "
            f"{registro['Descrição']}\n"
        )
    await update.message.reply_text(mensagem)

Esta função se conecta à API, lê todos os registros da planilha em uma lista denominada registros, e pega os últimos 5. Em seguida, é montada uma string com os dados desses últimos registros e então essa string é exibida como retorno de /last.

E agora, precisamos adicionar esses handlers ao bot. Isso é feito na função main(), incluindo estas linhas logo abaixo de app.add_handler():

app.add_handler(
        CommandHandler("help", help_command))
app.add_handler(
        CommandHandler("save", save_command))
app.add_handler(
        CommandHandler(
            "last", print_last_transactions))

O código completo do bot pode ser encontrado no GitHub.

Conclusão

Este bot foi projetado com o propósito de facilitar o registro de transações no cotidiano. Alguns cuidados, no entanto, precisamos ter. O primeiro deles é não expor dados sensíveis como senhas, tokens, entre outros. Procuramos orientar o uso de dotenv para manter esses dados em variáveis de ambiente. Nossas credenciais da API Google Sheets ainda estão em um arquivo, mas em breve mostraremos como passar a tratar esses dados também no dotenv.

Outra preocupação é que, ao você executar o código, seja pelo método polling ou webhook, seja no seu próprio computador ou em um serviço de nuvem, qualquer usuário do Telegram pode acessá-lo. Existem maneiras de evitar isso e, mesmo não tendo nos referido a isso no artigo, o código está presente na versão que compartilhamos no GitHub.

Por fim, vamos continuar trabalhando nesse projeto. E a próxima funcionalidade que será implementada consiste de automação com uma IA de modo que ela faça uma análise dos dados na planilha e produza um diagnóstico financeiro.

Comentários