https://a.storyblok.com/f/270183/35111/dfc944b669/manage-queues_1200x600.png

Use Python e Flask para gerenciar filas por SMS

Publicado em May 18, 2021

Tempo de leitura: 17 minutos

Embora muitas experiências humanas possam ser aprimoradas com o uso da tecnologia, há uma que está claramente pronta para uma solução moderna: ficar na fila. Seja para renovar sua carteira de motorista ou conseguir uma mesa em um restaurante badalado para o brunch, o tempo gasto amontoado em salas de espera com estranhos é algo que você provavelmente gostaria de evitar. Felizmente, a onipresença dos celulares significa que novos sistemas de fila por mensagem de texto e pela internet estão se tornando mais comuns — envie uma mensagem para reservar seu lugar na fila, verifique o status e receba uma notificação quando chegar à frente da fila.

Neste tutorial, vou mostrar a vocês como usar Python e o Flask para criar um sistema simples de gerenciamento de filas de SMS. Existem três componentes principais:

  • Um backend para responder a mensagens de texto e tomar as medidas adequadas

  • Uma página de status que pode ser acessada pela internet ou exibida em um quiosque

  • Uma página de gerenciamento que lista as pessoas na fila e permite que elas sejam notificadas ou removidas

Para simplificar, este aplicativo lidará apenas com as funções mais básicas, mas a estrutura que ele oferece deve facilitar a criação de um sistema robusto que atenda às suas necessidades.

Pré-requisitos

  • Python (este código foi testado com a versão 3.8)

  • ngrok

Configuração

Para começar, crie um novo diretório para o projeto e acesse-o:

mkdir sms-queue-notify cd sms-queue-notify

Se você preferir começar com o código final, pode clonar o projeto de exemplo no GitHub da Comunidade Nexmo:

git clone https://github.com/nexmo-community/sms-queue-notify.git cd sms-queue-notify

Quando estiver no diretório do seu projeto, crie e ative um ambiente virtual:

python3 -m venv venv source venv/bin/activate

O ambiente virtual ajuda você a gerenciar e isolar as dependências do seu projeto. Para instalar as dependências necessárias para este projeto, você precisará ter um requirements.txt arquivo. Se você clonou o repositório de exemplo, já está tudo pronto, mas se estiver criando do zero, seu arquivo deve ficar assim:

Flask==1.1.1
Flask-SQLAlchemy==2.4.1
nexmo==2.4.0

Para instalar essas dependências, execute o seguinte no diretório do seu projeto:

pip install -r requirements.txt

Inicialização do banco de dados

Para que seu aplicativo funcione corretamente, você precisará configurar um banco de dados para armazenar informações sobre as pessoas que estão na fila. Como os requisitos de armazenamento de dados para este projeto são relativamente simples, você utilizará o banco de dados integrado ao Python, o SQLite. Você gerenciará seu banco de dados com Flask-SQLAlchemy, que oferece uma camada sobre o banco de dados para que você possa fazer consultas com funções simples, em vez de escrever SQL.

Se você ainda não baixou o código do repositório de exemplos, crie um novo arquivo chamado main.py, contendo o seguinte:

from flask import Flask, render_template, request, Response
from flask_sqlalchemy import SQLAlchemy

db_path = "sqlite:///queue.db"

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = db_path

db = SQLAlchemy(app)

class User(db.Model):
    phone_number = db.Column(db.String, unique=True, nullable=False, primary_key=True)
    notified = db.Column(db.Integer)
    join_time = db.Column(db.DateTime)
    wait_time = db.Column(db.Integer)

if __name__ == '__main__':
    app.run(debug=True, threaded=True)

Este script configura sua aplicação Flask e a configuração básica do banco de dados, incluindo a definição de um nome e localização para o banco de dados, bem como a definição dos nomes e tipos de dados das colunas dentro do banco de dados. Depois de executar este script ou o de exemplo main.py, inicie o Python no modo interativo:

python

Em seguida, execute os seguintes comandos para configurar seu banco de dados:

>>> from main import db >>> db.create_all() >>> quit()

Configuração do ngrok e do Nexmo

Agora que você já tem um banco de dados pronto para uso, será necessário realizar algumas configurações básicas para usar o Nexmo no envio e recebimento de mensagens de texto. Como primeiro passo, certifique-se de que o ngrok esteja instalado e em execução na porta 5000:

ngrok http 5000

Quando o ngrok estiver em execução, será exibida uma URL para você usar no redirecionamento para o seu servidor local. Anote essa URL, pois você a utilizará em breve:

An example of an ngrok instance running

Na página principal do seu Painel do Nexmo, localize sua Chave e seu Segredo:

Screenshot of the Nexmo dashboard

Copie essas linhas e coloque-as no início do seu main.py arquivo, da seguinte maneira:

NEXMO_KEY = "<Your Nexmo Key>"
NEXMO_SECRET = "<Your Nexmo Secret>"

Em seguida, adicione esta linha:

client = nexmo.Client(key=NEXMO_KEY, secret=NEXMO_SECRET)

De volta ao painel, acesse Numbers -> Seus números (se você ainda não tiver um número, vá para Numbers -> Comprar números primeiro). Passe o mouse sobre o seu número até ver o botão “Copiar” . Clique nele e, em seguida, cole o número main.py assim:

NEXMO_NUMBER = "<Your Nexmo Number>"

Já que você está aqui, clique no ícone de engrenagem abaixo da seção coluna “Gerenciar” :

A screenshot of a page showing your rented numbers

No campo “URL do webhook de entrada” , insira o seguinte, usando a URL do ngrok que você obteve anteriormente:

<Your ngrok URL>/webhooks/inbound-sms

Defining webhook urls

Salve essa configuração.

Se você estiver trabalhando a partir do repositório de exemplo, agora já tem tudo o que precisa para executar o aplicativo. Digite o seguinte no terminal para iniciar seu servidor de desenvolvimento:

python main.py

Em um navegador, acesse a URL do ngrok para ver a tela de status ou acesse <ngrok url>/list para acessar a tela de gerenciamento. Em seguida, envie uma mensagem de texto com “Oi” para o número que você configurou para se adicionar à lista!

Para quem está desenvolvendo do zero, vamos passar para a criação da lógica de back-end.

Back-end

A estrutura de back-end desta aplicação é relativamente simples. Você criará quatro rotas — duas associadas às visualizações do front-end, um webhook para receber mensagens SMS recebidas e um stream para publicar eventos enviados pelo servidor. Além das rotas, você precisará de um conjunto de funções que executem ações com base nas mensagens recebidas e em outras entradas do usuário. Por fim, você terá algumas funções auxiliares para gerenciar consultas ao banco de dados e formatar texto.

Vamos começar pelas rotas para as visualizações. Adicione o seguinte main.py depois da seção em que você inicializa o banco de dados e antes de if __name__ == '__main__':

@app.route('/')
def index():
    return render_template('index.html', length=query_length(), number=phone_format(NEXMO_NUMBER))

@app.route('/list', methods=('GET', 'POST'))
def list():
    if request.method == 'POST':
        if 'notify' in request.form:
            notify(request.form['notify'])
        elif 'remove' in request.form:
            remove(request.form['remove'])
        elif 'arrived' in request.form:
            remove(request.form['arrived'])
    users = query_users()
    return render_template('list.html', users=users)

O index procedimento é simples: quando alguém acessar o site, verá o conteúdo da index.html página (ainda a ser criada), que incluirá informações sobre o comprimento da fila e o número de telefone para ligar (com a ajuda de funções que você criará em breve).

A list rota contém um pouco mais de informações, já que esta página permite a inserção de dados. Se a página for visualizada (uma solicitação GET), o visitante verá o conteúdo de list.html, completo com informações do usuário (números de telefone das pessoas na fila). Se uma solicitação POST for feita para essa rota por meio do formulário na página, as informações da solicitação serão usadas para determinar qual botão foi pressionado. A notify, remove, e query_users serão definidas em breve.

Agora que você já definiu essas rotas, é hora de adicionar a rota do webhook:

@app.route('/webhooks/inbound-sms', methods=('GET', 'POST'))
def inbound_sms():
    if request.is_json:
        message = request.get(json())
    else:
        message = dict(request.form) or dict(request.args)
    num = message['msisdn']
    text = message['text'].lower()
    map = {
        "hi": add,
        "cancel": remove,
        "status": status,
        "help": help
    }
    action = map.get(text)
    if action:
        action(num)
    else:
        send(num, "Could not understand. Please try again")
    return ('', 204)

Você talvez reconheça esse fluxo da etapa de configuração anterior. Quando a Nexmo receber uma mensagem SMS no número que você configurou com este webhook, você receberá um objeto de solicitação contendo o conteúdo da mensagem e informações sobre o remetente. Para nossos fins, extraímos o número de telefone do remetente e o texto da mensagem, mapeando o texto para as funções de ação apropriadas. Se não for possível analisar a mensagem, informamos o remetente para que ele possa tentar novamente.

Antes de criar a rota final, vamos definir nossas funções auxiliares e de ação. Inclua o seguinte main.py após as definições das rotas:

def phone_format(num):                                                
    return format(int(num[:-1]), ",").replace(",", "-") + num[-1]

def query_length():
    return User.query.filter(User.notified == 0).count()

def query_users():
    users_waiting = []
    users_notified = []
    for result in User.query.all():
        if result.notified == 0:
            time_diff = datetime.now() - result.join_time
            wait_time = divmod(time_diff.seconds, 60)[0]
            user = {"phone_number": str(result.phone_number), "wait_time": wait_time}
            users_waiting.append(user)
        else:
            wait_time = result.wait_time
            user = {"phone_number": str(result.phone_number), "wait_time": wait_time}
            users_notified.append(user)
    users = {"waiting": users_waiting, "notified": users_notified}
    return users

def send(num, text):    
    response = client.send_message({'from': NEXMO_NUMBER, 'to': num, 'text': text})
    response = response['messages'][0]
    if response['status'] == '0':
        print('Sent message', response['message-id'])
    else:
        print('Error:', response['error-text'])
    return

Essas funções auxiliares funcionam da seguinte maneira:

  • phone_format: Divida o número de telefone configurado com hífens para fins de exibição.

  • query_length: Consulte o banco de dados para verificar quantas pessoas estão na fila.

  • query_users: Consulte o banco de dados para obter todos os usuários e organize o resultado de forma que aqueles que estão na fila e aqueles que já foram notificados sejam agrupados separadamente.

  • send: Envie uma mensagem SMS com o número e o texto indicados.

Em seguida, adicione estas funções de ação:

def add(num):
    if User.query.get(num):
        send(num, "Hello again!")
        status(num)
    else:
        user = User(phone_number=num, notified=0, join_time=datetime.now())
        db.session.add(user)
        db.session.commit()
        send(num, "You've been added to the list")
        help(num)
    return

def remove(num):
    user = User.query.get(num)
    if user:
        db.session.delete(user)
        db.session.commit()
        send(num, "You've been removed from the list")
    else:
        print("User not found")
    return

def notify(num):
    user = User.query.get(num)
    if user.notified == 0:
        send(num, "Your turn")
        user.notified = 1
        time_diff = datetime.now() - user.join_time
        user.wait_time = divmod(time_diff.seconds, 60)[0]
        db.session.commit()
    else:
        print("User already notified")
    return
    
def status(num):
    user = User.query.get(num)
    if not user:
        send(num, "Not in line")
    elif user.notified == 1:
        send(num, "Notified")
    else:
        users = query_users()
        users_sorted = sorted(users["waiting"], key = lambda i: i['wait_time'], reverse = True)   
        i = 0
        while i < len(users_sorted):
            if users_sorted[i]["phone_number"] == num:
                i += 1
                break
            i += 1
        send(num, "Number " + str(i) + " of " + str(len(users_sorted)) + " in line")
    return

def help(num):
    send(num, "For updates, text 'status'nTo remove yourself from the list, text 'cancel'")
    return

Resumindo:

  • add: Adiciona um novo usuário quando for enviado “Oi”. Também retornará o status do usuário, caso ele já esteja na fila.

  • remove: Remove um usuário da lista. Pode ser acionado por meio de um botão na página de gerenciamento ou se o usuário enviar a mensagem “cancelar”.

  • notify: Avisa ao usuário que é a vez dele. É acionado por meio de um botão na página de gerenciamento. Além disso, calcula o tempo total de espera desse usuário e armazena o valor no banco de dados.

  • status: Informa ao usuário em que posição ele está na fila.

  • help: Fornece informações básicas sobre os comandos que o usuário pode enviar.

Com essas rotas e funções definidas, você já tem quase tudo o que precisa para um backend completo. A quarta e última rota nos permite atualizar dinamicamente as visualizações com base em mensagens do servidor, o que é útil quando pessoas são adicionadas ou removidas da lista por SMS:

@app.route("/stream")
def stream():
    def eventStream():
        line_length = query_length()
        yield "data: {}nn".format(json.dumps(query_users()))
        while True:
            new_line_length = query_length()
            if new_line_length != line_length:
                line_length = new_line_length
                yield "data: {}nn".format(json.dumps(query_users()))
            time.sleep(1)
    return Response(eventStream(), mimetype="text/event-stream")

Essa rota funciona de maneira diferente das anteriores. Depois de ser chamada inicialmente pelo cliente, a função continua em execução até que o servidor seja desligado. Isso permite que ela mantenha uma conexão com o cliente, enviando atualizações sempre que o número de pessoas na fila mudar.

Agora que você já tem todos os componentes do backend, é hora de criar as visualizações!

Front-end

Você precisa criar duas visualizações: uma para a página de status e outra para a página de gerenciamento. Comece criando um novo diretório, templates, e um novo arquivo dentro desse diretório, index.html. Nesse arquivo, insira o seguinte:

<!DOCTYPE html>
<html>

<head>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
</head>

<body>
  <div class="container-fluid center-block text-center">
    <div class="row">
      <div class="col-lg-12">   
        <p id="line_length"  style="font-size: 20vh">{{ length }}</p>
        <p style="font-size: 5vh">in line</p>
      </div>
    <div class="row">
      <div class="col-lg-12" style="height: 20vh">
    </div>
    <div class="row">
      <div class="col-lg-12">   
        <p class="lead" style="font-size: 7vh">Text <b>Hi</b> to <b>{{ number }}</b> to be added</p> 
      </div>
    </div>
  </div>  
</body>
<script>
    var targetContainer = document.getElementById("line_length");
    var eventSource = new EventSource("/stream");
    eventSource.onmessage = function(e) {
      var users = JSON.parse(e.data)
        targetContainer.innerHTML = users.waiting.length;
    };
</script>
<noscript>
    <meta http-equiv="refresh" content="30">
</noscript>

</html>

Algumas observações a serem feitas aqui. Primeiro, você verá que estamos usando o Bootstrap para aplicar alguns estilos básicos. Você pode ler mais sobre como usar o Bootstrap em uma de nossas posts de algumas semanas atrás. Em segundo lugar, observe o {{ length }} e {{ number }} , onde recebemos dados do backend quando o modelo é renderizado. Por fim, observe o pequeno trecho de JavaScript no final — ele se conecta ao nosso stream ponto de extremidade e processa os eventos enviados pelo servidor, atualizando dinamicamente o número de usuários na fila.

Há também uma pequena noscript seção, configurada para atualizar a página a cada trinta segundos caso o JavaScript esteja desativado. Da forma como este aplicativo foi configurado, ele continuará funcionando corretamente sem o JavaScript; apenas não será atualizado tão instantaneamente.

Para a página de gerenciamento, crie um novo arquivo list.html e inclua o seguinte:

<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
    </head>

    <body>
        <form method="POST" class="center-block text-center" style="width: fit-content">
            <h3>In Line</h3>
            <table id="notified" class="table table-condensed">
            {% for user in users.waiting %}
                <tr>
                    <td>{{ user.phone_number }}</td>
                    <td><button name="notify" value="{{ user.phone_number }}" class="btn btn-primary btn-xs" type="submit">Notify</button></td>
                    <td><button name="remove" value="{{ user.phone_number }}" class="btn btn-default btn-xs" type="submit">Remove</button></td>
                </tr>
            {% endfor %}
            </table>
            <h3>Notified</h3>
            <table class="table table-condensed">
            {% for user in users.notified %}
                <tr>
                    <td>{{ user.phone_number }}</td>
                    <td><button name="arrived" value="{{ user.phone_number }}" class="btn btn-primary btn-xs" type="submit">Arrived</button></td>
                </tr>
            {% endfor %}
            </table>
        </form>
    </body>

    <script>
        var targetContainer = document.getElementById("notified");
        var eventSource = new EventSource("/stream");
        eventSource.onmessage = function(e) {
            var user;
            var users = JSON.parse(e.data);
            users = users.waiting.sort((a, b) => (a.wait_time < b.wait_time) ? 1 : -1)
            var user_table = '';
            for (user of users){
                user_table = user_table + '<tr>'
                + '<td>' + user.phone_number + '</td>'
                + '<td><button name="notify" value="' + user.phone_number + '" class="btn btn-primary btn-xs" type="submit">Notify</button></td>' 
                + '<td><button name="remove" value="' + user.phone_number + '" class="btn btn-default btn-xs" type="submit">Remove</button></td>'
                + '</tr>'
            }
            targetContainer.innerHTML = user_table;
        };
    </script>
    <noscript>
        <meta http-equiv="refresh" content="30">
    </noscript>

</html>

Nesta página, você verá que há um formulário, que é usado para enviar dados quando os botões são clicados. Além disso, assim como a página de status, esta página inclui um código em JavaScript para processar eventos enviados pelo servidor, mas continuará funcionando corretamente mesmo que o JavaScript esteja desativado.

Dando o pontapé inicial

Com o backend e o frontend concluídos, agora você pode executar seu aplicativo! Como mencionado anteriormente, isso é feito da seguinte forma:

python main.py

Em um navegador, acesse a URL do ngrok para ver a tela de status:

A screenshot of the status

Acesse <ngrok url>/list para ver a visualização de gerenciamento. Em seguida, envie uma mensagem com o texto “Oi” para o número que você configurou para se adicionar à lista!

A screenshot of the list view

Próximos passos

Há várias maneiras de aprimorar esse aplicativo para oferecer uma experiência mais robusta. A mais óbvia é incluir uma estimativa do tempo de espera nas atualizações de status. Os tempos de espera para quem está na fila já são calculados e usados para ordenar a lista; portanto, basta escolher o algoritmo de sua preferência para fazer essas estimativas. Outra ideia seria oferecer mais opções além do SMS, como o Facebook Messenger ou o WhatsApp. Se você quiser seguir esse caminho, não deixe de conferir as API de Mensagens da Nexmo.

Se você tiver algum problema ou dúvida, entre em contato conosco pelo nosso Slack da Comunidade. Obrigado pela leitura!

Compartilhar:

https://a.storyblok.com/f/270183/384x384/fdfdc77b6d/zachwalchuk.png
Zach WalchukEx-funcionários da Vonage

Zach é um ex-educador de desenvolvedores da Vonage