https://a.storyblok.com/f/270183/178742/f264895dc1/python-dead-drop.png

Como criar um sistema de entrega secreta de mensagens de voz com Python e Flask

Publicado em May 13, 2021

Tempo de leitura: 13 minutos

Peguei o fone sujo do telefone público e disquei o número, como já tinha feito centenas de vezes antes.

"Aqui é a Oleg's Pizza. Deixe uma mensagem após o sinal."

Era só isso que dizia — nunca havia uma pessoa de verdade do outro lado da linha —, apenas uma voz robótica de uma empresa improvável.

[BIP] - em algum lugar, uma fita começou a gravar. Deixei minha mensagem.

"Oi, aqui é o Chuck. Gostaria de uma pizza de pepperoni e cogumelos, por favor."

Deixei o fone de lado e fui embora.

Meu nome não é Chuck e eu não gosto de pepperoni, mas isso deixaria a mensagem bem clara: meu disfarce foi descoberto e, na segunda-feira, eu já teria ido embora, restando apenas como uma lembrança que se desvanece na memória daqueles que me conheciam.


Adoro um bom thriller de espionagem, e parece que uma das partes mais difíceis de ser um espião é encontrar um ponto de entrega secreto para deixar mensagens para o seu contato. Felizmente, neste post, vou facilitar a vida de todos vocês, espiões, mostrando como criar um número de telefone secreto na internet, onde vocês podem deixar mensagens para que alguém as recupere mais tarde.

Pré-requisitos

Vou partir do princípio de que você já leu o post incrível do Aaron que descreve como usar o o Ngrok para desenvolver webooks. Se ainda não leu, vá ler agora mesmo — vale a pena.

Também vou partir do princípio de que você tem conhecimentos básicos de Python e Flask.

Recomendo instalar a ferramenta CLI da Vonage e ler a breve postagem no blog sobre como instalá-la — algumas das instruções abaixo farão uso dela, embora você possa realizar essas ações no Painel da Nexmo , se preferir.

O que você vai construir

Vou mostrar a vocês como criar um serviço básico de correio de voz que permita que as pessoas liguem para o seu número Nexmo e deixem uma mensagem.

A mensagem gravada será copiada para o seu servidor, e você criará uma página da web simples que exibe a lista das gravações e permite reproduzi-las no navegador.

Começando seu projeto

Se você preferir apenas acompanhar o meu código já existente, pode encontrá-lo aqui, mas recomendo que você acompanhe este post e crie o código por conta própria!

A estrutura da pasta do nosso projeto é a seguinte:

Initial project structure

Como este é um projeto pequeno, todo o seu código em Python ficará no answerphone/__init__.py, mas se fosse maior, você poderia dividi-lo em módulos separados dentro do answerphone pacote.

Você também colocará nossos recursos estáticos em static e seus modelos em templates e, assim, o Flask saberá onde encontrá-los.

Optei por salvar minhas gravações em MP3 em uma pasta no nível do projeto recordings , fora do answerphone pacote, pois é uma boa ideia separar os dados (especialmente aqueles baixados da Internet!) do código executável.

Não dá para ver na imagem acima, mas também há um .env arquivo no diretório do projeto, que contém toda a minha configuração.

Instalar dependências

No meu projeto, utilizei o pip-tools para fixar minhas dependências, mas se você nunca usou o pip-tools antes, recomendo que cole o seguinte diretamente no requirements.txt e, em seguida, execute pip install -r requirements.txt:

python-dotenv~=0.10 flask~=1.0 tinydb~=3.13 nexmo~=2.3

Um breve resumo das nossas dependências:

  • dotenv será usado para carregar a configuração do nosso .env arquivo de configuração.

  • flask é nossa estrutura de web e nosso servidor de desenvolvimento web.

  • tinydb é um banco de dados bem simples que armazena todos os seus dados no formato JSON.

  • nexmo é a biblioteca cliente do Nexmo para Python e simplifica o uso das APIs do Nexmo em comparação com a implementação manual.

Abra __init__.py no seu answerphone pacote e digite o seguinte:

from flask import Flask

@app.route("/answer", methods=["GET", "POST"])
def answer():
    """
    An NCCO webhook, providing actions that tell Nexmo to read a statement
    to the user and then record a message.
    """
    return jsonify(
        [
            {
                "action": "talk",
                "text": "<speak>You have reached <phoneme alphabet="ipa" ph="əʊlɛgz">Oleg's</phoneme> pizza. Please leave a message after the beep.</speak>",
                "voiceName": "Brian",
            },
            {
                "action": "record",
                "beepStart": True,
                "eventUrl": [ "https://example.com/recording" ],
                "endOnSilence": 3,
            },
        ]
    )

Certifique-se de que o Ngrok esteja em execução e inicie seu servidor de desenvolvimento com:

FLASK_ENV=development FLASK_APP=answerphone flask run

Agora, se você acessar https://your-random-id.ngrok.io/answer com seu navegador, você deverá ver algo parecido com o seguinte:

[
    {
        "action": "talk",
        "text": "You have reached Oleg's pizza. Please leave a message after the beep.",
        "voiceName": "Brian"
    },
    {
        "action": "record",
        "beepStart": true,
        "eventUrl": [
            "https://example.com/recording"
        ],
        "endOnSilence": 3
    }
]

Agora, vamos criar um aplicativo de Voice e vincular um número a essa URL. No seu console, execute a ferramenta CLI da Vonage, que o guiará passo a passo na criação do seu aplicativo:

# Create an app vonage apps:create

Isso exibirá algo como Application created: 26aa5db4-546a-11e9-8f2d-0f348a273d3a, e criará um arquivo chamado private.key no seu diretório atual.

Pegue esse ID e cole-o em um novo .env arquivo assim:

NEXMO_PRIVATE_KEY="./private.key" NEXMO_APPLICATION_ID=26aa5db4-546a-11e9-8f2d-0f348a273d3a

Deixe isso de lado por enquanto — explicarei como carregar a configuração daqui a pouco.

Se você precisar comprar um número, recomendo que faça isso no Painel da Nexmo.

Depois de comprar um número (certifique-se de que ele ofereça suporte a chamadas de Voice!), volte à sua linha de comando e use o nexmo comando para vincular o número ao seu aplicativo:

# Replace the phone number with your own # and the application ID with your application ID! nexmo link:app 447700900606 26aa5db4-546a-11e9-8f2d-0f348a273d3a

Agora, se você ligar para o seu número da Nexmo, deverá ouvir a mensagem na talk ação acima: “Você ligou para a Pizzaria do Oleg. Por favor, deixe uma mensagem após o sinal.” Ok!

Verifique os logs do Ngrok. Você pode notar alguns erros 404 em /event. Não se preocupe com isso agora — você adicionará um webhook de evento mais adiante neste tutorial.

Infelizmente, assim que a Nexmo terminar de gravar sua mensagem, ela atualmente envia uma solicitação POST para a URL indicada em sua record ação, que está definida como https://example.com/recording.

Vamos corrigir isso para que você possa receber o evento de gravação e baixar o MP3, de modo que seu manipulador possa captar as mensagens dos agentes.

No seu __init__.py, adicione o seguinte:

# Add to your imports:
from dotenv import load_dotenv
from flask import request, url_for
import nexmo

# After your imports:
load_dotenv()   # Loads .env config into `os.environ`

client = nexmo.Client(
    application_id=os.environ["NEXMO_APPLICATION_ID"],
    private_key=os.environ["NEXMO_PRIVATE_KEY"],
)

@app.route("/new-recording", methods=["POST"])
def new_recording():
    recording_bytes = client.get_recording(request.json['recording_url'])
    recording_id = request.json['recording_uuid']
    with open(f"recordings/{recording_id}.mp3", 'wb') as mp3_file:
        mp3_file.write(recording_bytes)
    return ""

e agora modifique seu answer webhook. A segunda ação deve ficar assim:

{
    "action": "record",
    "beepStart": True,
    "eventUrl": [url_for("new_recording", _external=True)],
    "endOnSilence": 3,
},

Agora você está usando a função url_for para obter uma URL que aponta para o new_recording webhook que você acabou de adicionar ao arquivo.

Certifique-se de que sua recording pasta existe e, em seguida, reinicie o servidor de desenvolvimento do Flask.

Agora, quando você ligar para o seu número da Nexmo e deixar uma mensagem, você deverá encontrar um arquivo MP3 na recording pasta. Abra-o no seu reprodutor de MP3 favorito para ouvir o que está gravado!

Se quiser, você pode parar por aqui — você já aprendeu tudo o que é básico sobre como fazer com que o Nexmo grave uma mensagem e, em seguida, como baixar essa mensagem para o seu servidor (o Nexmo armazena a gravação para você apenas por algumas horas).

Mas seria uma boa ideia armazenar alguns metadados junto com o áudio, para que você saiba quem era o autor da ligação e quando ela ocorreu. Dessa forma, você pode adicionar uma página listando todas as chamadas à sua caixa postal.

Eu escolhi o TinyDB para fazer isso — é um pequeno banco de dados bem simples que salva seus dados em um arquivo JSON. Não é muito rápido e não lida muito bem com grandes volumes de dados, mas serve perfeitamente para este projeto!

Adicione o seguinte ao seu .env arquivo: DATABASE_PATH=answerphone.db.

Você instrui o TinyDB a armazenar dados neste arquivo inserindo o seguinte no início do seu __init__.py arquivo:

from tinydb import TinyDB, Query

db = TinyDB(os.environ["DATABASE_PATH"])

Agora adicione o seguinte, para criar duas “tabelas” para armazenar os dados do chamador e os dados da gravação:

calls = db.table('calls')
recordings = db.table('recordings')

Agora você precisa fazer duas coisas: responder aos eventos de chamada e registrar os dados da chamada quando ela for atendida; além disso, é preciso adicionar algumas linhas ao seu recording webhook para que ele armazene os dados da gravação no banco de dados.

Primeiro, adicione o event webhook:

@app.route("/event", methods=["POST"])
def event():
    if request.json.get('status') == 'answered':
        calls.insert(request.json)

    return ""

A linha calls.insert(request.json) armazena todos os dados JSON da solicitação na calls tabela que você criou acima.

Agora, adicione uma linha semelhante ao seu recording webhook, após o código para salvar o arquivo MP3 na sua recordings pasta:

...

with open(f"recordings/{recording_id}.mp3", 'wb') as mp3_file:
    mp3_file.write(recording_bytes)

recordings.insert(request.json)

return ""

Ligue novamente para o seu número da Nexmo e deixe uma mensagem. Verifique se tudo funciona sem erros.

Se você der uma olhada lá dentro answerphone.db , você deverá ver uma grande quantidade de dados JSON armazenados. Agora, vamos carregar esses dados em uma bela página da web!

Primeiro, adicione uma visualização que permita carregar um arquivo MP3 no navegador:

@app.route("/recordings/<uuid>")
def recording(uuid):
    response = make_response(open(f'recordings/{uuid}.mp3', 'rb').read())
    response.headers['Content-Type'] = 'audio/mpeg'
    return response

O código acima abre o arquivo binário MP3, gera uma resposta a partir dos bytes e, em seguida, define o cabeçalho de tipo de conteúdo como 'audio/mpeg', que é o tipo correto para dados MP3.

Você pode testar isso acessando a URL “/recordings/you-uuid-goes-here” usando o ID de um dos arquivos MP3 da sua pasta de gravações.

Agora você deve adicionar uma visualização que liste todas as gravações, juntamente com alguns dos dados das chamadas associados a cada gravação.

Isso pode ser facilitado com uma pequena classe auxiliar.

Coloque este código no início do seu __init__.py arquivo:

class Recording:
    def __init__(self, data):
        self.uuid = data['recording_uuid']
        related_calls = calls.search(Query().conversation_uuid == data['conversation_uuid'])
        if related_calls:
            self.related_call = related_calls[0]
        else:
            self.related_call = None

Essa classe foi projetada para ser inicializada usando os dados JSON fornecidos ao recording ponto de extremidade e armazenados na recordings tabela em nosso banco de dados.

Ele consulta automaticamente os dados da chamada associados na calls tabela e os adiciona ao objeto Gravação como o related_call atributo.

Agora escreva o seguinte código da visualização, que passa uma Recording instância para a visualização para cada gravação armazenada no banco de dados:

@app.route("/")
def index():
    """
    A view which lists all stored recordings.
    """
    return render_template("index.html.j2", recordings=[Recording(r) for r in recordings])

Isso não vai funcionar no momento, porque você ainda não criou um arquivo de modelo!

Crie um arquivo em answerphone/templates/index.html.j2 e coloque algo como o seguinte nele:

<!doctype html>
<html>
    <head>
        <title>Oleg's Pizza</title>
    </head>
    <body>
        <h1><i>"Oleg's Pizza"</i><br>Dead Drop Recordings</h1>
        {% for recording in recordings -%}
            <h2>Call From: <em>{{ recording.related_call.from }}</em></h2>
            <p><strong>When:</strong> {{ recording.related_call.timestamp }}</p>
            <a href="/recordings/{{ recording.uuid }}">Listen</a>
        {% endfor -%}
    </body>
</html>

Agora, se você acessar seu https://localhost:5000/ você deverá ver algo parecido com o seguinte:

Recording list

Agora você é um mestre dos serviços secretos!

Vou resumir o que você acabou de fazer:

  • Você atendeu uma chamada recebida realizando algumas ações do NCCO.

  • Você solicitou à Nexmo que gravasse parte de uma ligação telefônica

  • Você concluiu o processo de gravação para baixar o arquivo MP3 criado.

  • Você armazenou os dados das chamadas em um banco de dados e criou uma lista de reprodução que pode ser acessada pela web!

Mais informações

Se você quiser se aprofundar um pouco mais no que acabou de aprender, as informações a seguir podem ser úteis:

Além disso, confira o repositório no GitHub deste projeto, pois documentei o código e aprimorei a visualização da lista.

Próximos passos

Existem algumas maneiras de levar esse projeto adiante. Você poderia usar um WebSocket para notificar o navegador quando uma nova gravação for disponibilizada, de modo que o manipulador não precise recarregar o navegador para receber mensagens do seu agente.

Você também pode usar uma SMS API da Nexmo para enviar uma mensagem SMS ao responsável quando uma nova gravação estiver disponível!

Se você criar algo legal, mande um e-mail para devrel@nexmo.com para nos contar!

Compartilhar:

https://a.storyblok.com/f/270183/150x150/a3d03a85fd/placeholder.svg
Mark SmithEx-funcionários da Vonage

Mark era o responsável nominal pelas bibliotecas de clientes da Nexmo (embora ele só desenvolva as bibliotecas em Python e Java). Ele começou como desenvolvedor Java, já trabalha com Python há 18 anos e vem se aventurando cada vez mais com Go e Rust. Ele gosta de levar as linguagens de programação ao limite e, depois, ensinar essas técnicas a outros programadores. Ele tem um chapéu de viking, mas não é um viking, e no Twitter usa o nome Judy2k por motivos que prefere não revelar.