https://d226lax1qjow5r.cloudfront.net/blog/blogposts/pycascades-code-of-conduct-hotline-nexmo-voice-api-dr/PyCascades-CoC-Hotline.png

Mejora de la línea directa del Código de Conducta de PyCascades con Nexmo Voice API

Publicado el May 4, 2021

Tiempo de lectura: 12 minutos

Hola, me llamo Mariatta. Trabajo como Ingeniera de Plataforma en Zapier. Soy Python Core Developer, y también ayudo a organizar las PyCascades conferencia.

En PyCascades, la diversidad en nuestra comunidad es una prioridad, no una ocurrencia tardía. Una de las formas en que intentamos conseguirlo es mediante un sólido código de conducta y su aplicación. Para facilitar la denuncia de problemas relacionados con el código de conducta, Alan Vezina, uno de nuestros organizadores, creó una línea directa del código de conducta. Desde entonces, la línea directa ha sido adoptada por PyCon US 2018 y DjangoCon 2018.

Así es como funciona la primera línea directa del CdC de PyCascades. Cuando alguien desee informar de un problema relacionado con el código de conducta, puede llamar al número de la línea directa. En ese momento, todos nuestros organizadores serán notificados, y la persona que llame será conectada con el primer organizador que responda. Para garantizar la rendición de cuentas, la información sobre la llamada también se publica en un canal de Slack, de modo que quede constancia de la misma.

Desde el lanzamiento de la línea directa, he estado pensando en ideas para mejorarla el año que viene.

En esta entrada del blog, voy a mostrarte cómo he utilizado Nexmo Voice API y Zapier para mejorar la línea directa del Código de Conducta de PyCascades.

Estas son las características de la línea directa mejorada:

  • Se saluda a la persona que llama, haciéndole saber que se ha puesto en contacto con la Línea Directa del Código de Conducta de PyCascades. Es importante que la persona que llama sepa que se ha puesto en contacto con el número correcto, la línea directa oficial del Código de Conducta de PyCascades.

  • Todas las llamadas se graban automáticamente. Informar sobre el Código de Conducta es un asunto importante y delicado. Disponer de una grabación nos ayuda a rendir cuentas, además de permitirnos volver atrás y reproducir la llamada para no perdernos ningún detalle.

  • La música de espera suena ahora mientras la persona que llama espera a ser conectada con uno de nuestros empleados.

  • Cuando un organizador responde a la llamada, la persona que llama escuchará un mensaje que le identifica: "Mariatta se une a esta llamada".

  • Hemos añadido una alerta para que el organizador sepa que esta llamada está relacionada con el Código de Conducta de PyCascades. Filtro mis llamadas. A menudo ignoro las llamadas de números 1-800 o de desconocidos cuyo número no reconozco. Durante el periodo de conferencias, necesito saber si estas llamadas se refieren a cuestiones relacionadas con el Código de Conducta (que debería atender), o si se trata de una llamada de telemarketing ofreciéndome un crucero gratuito (que ignoraré).

  • Ahora se lleva un registro de todas las actividades de las convocatorias del CdC en una hoja de cálculo de Google.

Además, aún debe funcionar la siguiente función:

  • Información sobre las llamadas entrantes a la línea directa de CoC publicada en Slack. Slack es una de las principales vías de comunicación de los organizadores de PyCascades. El mensaje de Slack sirve tanto de notificación como de registro de que se ha producido una llamada, aunque nadie haya respondido.

Otros datos técnicos:

La línea directa está escrita en Python, y mi framework web de elección es aiohttpun servidor y cliente web asíncrono para Python. He utilizado aiohttp para crear bots de GitHub como miss-islington y black-out.

El servicio web se despliega en Heroku. La mayor parte de la infraestructura web de The PSF está alojada en Heroku, por lo que, como desarrollador del núcleo de Python, he estado más familiarizado con Heroku que con otros tipos de infraestructura en la nube.

Una de las principales razones para elegir Nexmo API, para mí, es porque Nexmo ha apoyado a la comunidad Python de muchas maneras, incluyendo el patrocinio de PyCon US 2018, DjangoCon 2018 y el primer PyCascades ? La otra razón para elegir Nexmo API es porque la biblioteca nexmo-python está disponible en código abierto, y es compatible con las versiones más recientes de Python y probada contra Python 3.7.

Puede consultar el código fuente de la Línea directa de CdC mejorada.

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

Configuración de la aplicación Nexmo Voice

En primer lugar, le mostraré cómo está configurada mi aplicación Nexmo Voice.

Nexmo Voice App settings screenshotNexmo voice app settings

Al configurar una aplicación Voice en Nexmo, debe configurar dos URL de webhook, una URL de evento y una URL de respuesta.

La URL de Evento es siempre necesaria; aquí es donde Nexmo le enviará información cada vez que se produzca un cambio en el estado de la llamada.

Recepción del webhook de eventos y registro de las actividades

A continuación se muestra un ejemplo de carga útil entregada por el webhook Evento:

{
    "status": "started",
    "direction": "outbound",
    "from": "12025550124",
    "uuid": "80c80c80-80ce-80c8-80c8-80c80c80c80c",
    "conversation_uuid": "CON-be2be2be-a0dd-a0dd-a0dd-34b34b34b34b",
    "timestamp": "2018-10-25T17:42:17.552Z",
    "to": "12025550124"
}

Contiene información como el número de la persona que llama, qué número se ha marcado, el estado de la llamada, la marca de tiempo e identificadores únicos de llamada y conversación. Se trata de información útil que puede registrarse para tener constancia de cada actividad.

En lugar de crear mi propio servicio web para recibir estos webhooks, he creado una integración Zapier. Una de las integraciones que puedes utilizar en Zapier es Webhooks por Zapier. Con Webhooks by Zapier, puedes recibir datos de cualquier servicio o enviar solicitudes a cualquier URL sin escribir código ni ejecutar servidores. En otras palabras, puedes recibir y dar webhooks.

Cuando creé un nuevo Zap usando Webhooks by Zapier como la acción desencadenante, Zapier generó una URL "hooks.zapier.com" que puedo usar para recibir los webhooks. Proporcioné la URL hooks.zapier.com como la URL de Eventos en Nexmo Voice Application.

Webhook Trigger ScreenshotWebhook Trigger

Ahora que he configurado Zapier para recibir el webhook de eventos de Nexmo, puedo hacer muchas cosas. En primer lugar, he añadido una integración con Slack, para que se publique automáticamente un mensaje en nuestro canal privado de CoC sobre las llamadas entrantes a la línea directa. A continuación, añadí una integración con Google Sheets, de modo que cualquier actividad relacionada con la línea directa se añade automáticamente como una nueva fila en Google Sheets.

CoC Events screenshotCoC Events

Contestación de llamadas

Cuando una persona que llama marca el número de la línea directa, Nexmo enviará la carga útil de ese evento a la URL de respuesta. La URL de respuesta debe devolver un NCCO (Objeto de Control de Llamada Nexmo) que gobierna esta llamada.

La URL de la respuesta es la URL de mi servicio web /webhook/answer/ DE MI SERVICIO WEB. (código fuente)

Quería que se saludara a la persona que llama y se le notificara que se ha puesto en contacto con la Línea Directa del Código de Conducta de PyCascades. Por lo tanto, la primera NCCO que devolví es una acción de "hablar":

ncco = [
        {
            "action": "talk",
            "text": "You've reached the PyCascades Code of Conduct Hotline. This call is recorded."
        }
    ]

A continuación, como ahora recibo eventos cuando una persona llama a la línea directa, tengo que llamar a todos los miembros de nuestro personal y conectarlos a la misma llamada. Para ello, tengo que añadir a la persona que llama y al personal a una multiconferencia.

Para añadir interlocutores a una multiconferencia, añadiré una acción NCCO de "conversación" con el mismo nombre.

{
            "action": "conversation",
            "name": conversation name,
}

Entonces, ¿tengo que inventarme un "nombre" para la conversación? No necesariamente. Eche un vistazo a la carga útil de la URL de respuesta

Un ejemplo de solicitud GET a answer_url es el siguiente:

/webhooks/answer?to=447700900000&from=447700900001&conversation_uuid=CON-aaaaaaaa-bbbb-cccc-dddd-0123456789ab&uuid=aaaaaaaa-bbbb-cccc-dddd-0123456789cd

Observe que la carga útil incluye un archivo conversation_uuid. En lugar de "inventar" nuevos nombres para la conversación, decidí utilizar el mismo conversation_uuid como nombre de la conversación.

Así que recuperé el conversation_uuid de la solicitud y lo utilicé en la OCNC.

conversation_uuid = request.rel_url.query["conversation_uuid"].strip()
    ...
    {
            "action": "conversation",
            "name": conversation_uuid,
            ...
    }

Para grabar la conversación, puedo especificar "record": True en el diccionario NCCO de la conversación. Cuando finalice la grabación, Nexmo también enviará un webhook a la dirección eventUrly la carga útil de este webhook incluirá la url donde se almacena la grabación.

El siguiente es un ejemplo de carga útil para la grabación eventUrl webhook:

{
  "start_time": "2020-01-01T12:00:00Z",
  "recording_url": "https://api.nexmo.com/media/download?id=aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
  "size": 12345,
  "recording_uuid": "aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
  "end_time": "2020-01-01T12:01:00Z",
  "conversation_uuid": "bbbbbbbb-cccc-dddd-eeee-0123456789ab",
  "timestamp": "2020-01-01T14:00:00.000Z"
}

Una vez más, en lugar de configurar mi propio servicio web para recibir el webhook, hice uso de Webhooks por Zapier. Creé un Zap diferente para recibir los webhooks de grabación.

Recording Zapier screenshotZapier Recording

En este Zap, he añadido una integración con Google Spreadsheet, de modo que la información de la carga útil, incluyendo el archivo recording_urlse añaden automáticamente como una nueva fila en Google Spreadsheets. Además, he añadido una integración Slack para que los miembros de nuestro personal sean notificados de la nueva grabación.

En este punto, la conversación OCN tiene el siguiente aspecto:

conversation_uuid = request.rel_url.query["conversation_uuid"].strip()
    ...
    {
            "action": "conversation",
            "name": conversation_uuid,
            "record": True,
            "eventUrl": [os.environ.get("ZAPIER_CATCH_HOOK_RECORDING_FINISHED_URL")],

            ...
    }

En este punto, necesito que la línea directa haga dos cosas. En primer lugar, necesito que llame a cada miembro del personal para poder añadirlos a la conversación. Y en segundo lugar, necesito que reproduzca música mientras la persona que llama espera a ser conectada.

Reproduce música mientras la persona que llama espera

Reproducir música en esta llamada es bastante sencillo. Añada el icono "musicOnHoldURL" a la OCNC y proporcione una URL a la música que desea reproducir, por ejemplo:

"musicOnHoldUrl": ["https://..../music.mp3"]

Estoy usando la música de Colección de música de Wistiaespecíficamente de The Let 'Em In Sessions. Puedes ver la licencia de esta música aquí.

import random
    
    MUSIC_WHILE_YOU_WAIT = [
        "https://assets.ctfassets.net/j7pfe8y48ry3/530pLnJVZmiUu8mkEgIMm2/dd33d28ab6af9a2d32681ae80004886e/oaklawn-dreams.mp3",
        "https://assets.ctfassets.net/j7pfe8y48ry3/2toXv1xuOsMm0Yku0YEGya/a792ce81a7866fc77f6768d416018012/broken-shovel.mp3",
        "https://assets.ctfassets.net/j7pfe8y48ry3/16VJzaewWsKWg4GsSUiwGi/9b715be5e8c850e46de98b64e6d31141/lennys-song.mp3",
        "https://assets.ctfassets.net/j7pfe8y48ry3/1qApZVYkxaiayA6aysGAOo/8983586c8ab4db8b69490718469a12f5/new-juno.mp3",
        "https://assets.ctfassets.net/j7pfe8y48ry3/6iXXKtJCp2oCMiGmsmAKqu/8163a8fe863405292ba3609193593add/davis-square-shuffle.mp3",
    ]
    
    ncco = {
        ...
        "musicOnHoldUrl": [random.choice(MUSIC_WHILE_YOU_WAIT)],
    }

Convocar a los demás miembros del personal a la conferencia telefónica

Ahora tengo que llamar a cada uno de los miembros del personal y añadirlos a esta convocatoria.

Esto no es algo que pueda conseguir en la OCNC. Así que utilicé el cliente Nexmo Python de Nexmo Python. Se puede instalar usando pipasí que añadí nexmo a mi archivo requirements.txt.

He creado una función de ayuda para instanciar el cliente.

def get_nexmo_client():
        app_id = os.environ.get("NEXMO_APP_ID")
        private_key = os.environ.get("NEXMO_PRIVATE_KEY_VOICE_APP")
    
        client = nexmo.Client(application_id=app_id, private_key=private_key)
        return client

También he creado una función de ayuda para recuperar los números de teléfono del personal. Los números de teléfono se almacenan como variables de entorno en Heroku, en el siguiente formato:

[
            {
                "name": "Mariatta",
                "phone": "12025550124"
            },
            {
                "name": "Miss Islington",
                "phone": "12025550123"
            }
        ]

La función de ayuda es bastante sencilla:

import json
    
    def get_phone_numbers():
        return json.loads(os.environ.get("PHONE_NUMBERS"))

Ahora que tengo funciones para recuperar el cliente nexmo, así como los números de teléfono para marcar, puedo marcar los números.

Para llamar a un número utilizando la librería cliente Nexmo Python:

response = client.create_call({
      'to': [{'type': 'phone', 'number': 12025550124}],
      'from': {'type': 'phone', 'number': 12025550123},
      'answer_url': ['https://example.com/answer']
    })

En el método create_call método de llamada, necesitaba proporcionar el to número de teléfono, así como el from número de teléfono. El número de teléfono de to es el número de teléfono del personal al que quiero llamar.

Para el from número, en lugar de dar el número de teléfono de la persona que llama a la línea directa, he utilizado el propio número hotline Así, el personal sabe por el identificador de llamadas que se trata de la línea directa.

¿Qué pasa con el answer_url? El answer_url es el webhook para cuando un miembro del personal responde a esta llamada. El comportamiento deseado aquí es que el personal que respondió a la llamada se añada a la conversación en la que se encuentra la persona que llama a la línea directa. Por lo tanto, además de la carga útil proporcionada por Nexmo al webhook, tengo que pasar el archivo conversation_name (que es el conversation_uuid).

He creado un nuevo punto final en mi servicio web para gestionar este webhook incluyendo tanto el botón conversation_uuid y la llamada uuid en la URL:

@routes.get(
        "/webhook/answer_conference_call/{origin_conversation_uuid}/{origin_call_uuid}/"
    )
    async def answer_conference_call(request):
    
        origin_conversation_uuid = request.match_info["origin_conversation_uuid"]
        origin_call_uuid = request.match_info["origin_call_uuid"]
        ...

Con este endpoint creado, cada vez que un miembro del personal responda a la llamada de la línea directa, tendré una forma de saber a qué conversación añadirlo.

Por último, el webhook de respuesta tiene el siguiente aspecto:

@routes.get("/webhook/answer/")
async def answer_call(request):
    conversation_uuid = request.rel_url.query["conversation_uuid"].strip()
    call_uuid = request.rel_url.query["uuid"].strip()

    ncco = [
        {
            "action": "talk",
            "text": "You've reached the PyCascades Code of Conduct Hotline. This call is recorded.",
        },
        {
            "action": "conversation",
            "name": conversation_uuid,
            "record": True,
            "eventMethod": "POST",
            "musicOnHoldUrl": [random.choice(MUSIC_WHILE_YOU_WAIT)],
            "eventUrl": [os.environ.get("ZAPIER_CATCH_HOOK_RECORDING_FINISHED_URL")],
            "endOnExit": False,
            "startOnEnter": False,
        },
    ]

    client = get_nexmo_client()
    phone_numbers = get_phone_numbers()

    for phone_number_dict in phone_numbers:
        client.create_call(
            {
                "to": [{"type": "phone", "number": phone_number_dict["phone"]}],
                "from": {
                    "type": "phone",
                    "number": os.environ.get("NEXMO_HOTLINE_NUMBER"),
                },
                "answer_url": [
                    f"https://mariatta-enhanced-coc.herokuapp.com/webhook/answer_conference_call/{conversation_uuid}/{call_uuid}/"
                ],
            }
        )
    return web.json_response(ncco)

Añadir miembros del personal a la teleconferencia

El endpoint answer_conference_call endpoint se creó con el propósito de añadir a los miembros del personal a la multiconferencia. Para lograrlo, necesitaba devolver un NCCO al webhook que contiene una acción "conversación" y el nombre de la conversación. Pero antes de que se añadan, me gustaría saludar al personal para que sepan que se están uniendo a la Línea Directa del Código de Conducta de PyCascades.

Recuerde que la variable de entorno PHONE_NUMBERS también incluye los nombres de los propietarios de los números de teléfono.

He creado la siguiente función para recuperar el nombre del propietario del número de teléfono:

def get_phone_number_owner(phone_number):
        phone_numbers = get_phone_numbers()
        for phone_number_info in phone_numbers:
            if phone_number_info["phone"] == phone_number:
                return phone_number_info["name"]
    
        return None

Con esa función, puedo saludar al personal de la siguiente manera:

@routes.get(
        "/webhook/answer_conference_call/{origin_conversation_uuid}/{origin_call_uuid}/"
    )
    async def answer_conference_call(request):
    
        to_phone_number = request.rel_url.query["to"]
        origin_conversation_uuid = request.match_info["origin_conversation_uuid"]
    
        phone_number_owner = get_phone_number_owner(to_phone_number)
    
        ncco = [
            {
                "action": "talk",
                "text": f"Hello {phone_number_owner}, connecting you to PyCascades hotline.",
            },
            {
                "action": "conversation",
                "name": origin_conversation_uuid,
                "startOnEnter": True,
                "endOnExit": True,
            },
        ]
        return web.json_response(ncco)

En este momento, puede que se pregunte para qué sirve el origin_call_uuid se utiliza. Pensé que podría ser una buena cortesía para que la persona que llama a la línea directa sepa qué miembro del personal de PyCascades está respondiendo a su llamada. Además, recuerde que se trata de una conferencia telefónica, por lo que potencialmente más de una persona puede unirse. En lugar de dejar que alguien se una en silencio, voy a avisar a todos los participantes de quién se acaba de unir.

client = get_nexmo_client()

    response = client.send_speech(
        origin_call_uuid, text=f"{phone_number_owner} is joining this call."
    )

Así que ahora el answer_conference_call endpoint tiene el siguiente aspecto:

@routes.get(
        "/webhook/answer_conference_call/{origin_conversation_uuid}/{origin_call_uuid}/"
    )
    async def answer_conference_call(request):

        to_phone_number = request.rel_url.query["to"]
        origin_conversation_uuid = request.match_info["origin_conversation_uuid"]
        origin_call_uuid = request.match_info["origin_call_uuid"]
    
        phone_number_owner = get_phone_number_owner(to_phone_number)
        client = get_nexmo_client()
    
        try:
            response = client.send_speech(
                origin_call_uuid, text=f"{phone_number_owner} is joining this call."
            )
        except nexmo.Error as er:
            print(
                f"error sending speech to {origin_call_uuid}, owner is {phone_number_owner}"
            )
            print(er)
    
        else:
            print(f"Successfully notified caller. {response}")
    
        ncco = [
            {
                "action": "talk",
                "text": f"Hello {phone_number_owner}, connecting you to PyCascades hotline.",
            },
            {
                "action": "conversation",
                "name": origin_conversation_uuid,
                "startOnEnter": True,
                "endOnExit": True,
            },
        ]
        return web.json_response(ncco)

Flujo de llamadas completado

Con esto se completa el Código de Conducta mejorado de PyCascades.

El flujo de llamada completo es el siguiente:

  • Una persona llama a la línea directa.

  • Los miembros del personal de PyCascades reciben una notificación en Slack de que hay una llamada entrante a la línea directa.

  • La información sobre la convocatoria se añade a Google Sheets.

  • La persona que llama escucha un mensaje: "Bienvenido a la Línea Directa del Código de Conducta de PyCascades. Esta llamada está grabada".

  • La persona que llama escucha música mientras espera a ser conectada.

  • Todos los empleados de PyCascades reciben una llamada de la línea directa.

  • Un miembro del personal de PyCascades responde a la llamada y oye: "Hola {nombredelpersonal}, le pongo en contacto con la línea directa de PyCascades".

  • Mientras tanto, la persona que llama oye el mensaje "{nombredelpersonal} se une a esta llamada".

  • El personal y la persona que llama continúan la conversación.

  • El personal cuelga, momento en el que finaliza la grabación de la llamada.

  • Los miembros del personal reciben una notificación en Slack de que hay una nueva grabación.

  • La información sobre la grabación también se añade en Google Sheets.

Descargar la grabación

La grabación puede descargarse utilizando el cliente Python de Nexmo, y la dirección recording_url es la url recibida en el webhook de eventos de grabación.

client = get_nexmo_client()
    recording = client.get_recording(recording_url)

Las grabaciones de llamadas se almacenan en Nexmo durante un mes antes de que se eliminen automáticamente. Dado que estas llamadas son importantes y no queremos perder las grabaciones, he creado un script de línea de comandos que se puede utilizar para descargar las grabaciones.

El script puede ejecutarse del siguiente modo:

python3 -m download_recording url1 url2 url3 ...

Una vez ejecutado el script, las grabaciones se descargan y almacenan localmente en el directorio recording directorio.

Conclusiones

Gracias a Nexmo y Zapier, puedo mejorar la línea directa del Código de Conducta de PyCascades. La configuración de esta línea directa sí parece ser más complicada que antes de.

Sin embargo, creo que las nuevas mejoras como la auto-grabación, auto-registro en Google Spreadsheets son útiles para todos los miembros de nuestro personal, así que estoy dispuesto a pasar el tiempo extra para configurar esto para PyCascades. Además, al utilizar Zapier en lugar de codificarlo, podemos ser más flexibles en caso de que queramos añadir integraciones adicionales.

Gracias por leernos. Si usted tiene más preguntas, con respecto a la línea directa, PyCascades, o Zapier, por favor no dude en enviarme un correo electrónico a mariatta.wijaya@zapier.com


Nota de Nexmo Developer Relations: Estamos muy contentos de que Mariatta haya decidido utilizar Nexmo para ayudar a mejorar los informes del Código de Conducta de PyCascades. Creemos que tener un CoC es una parte vital de la creación de un espacio acogedor e inclusivo. Nos gustaría mostrar nuestro apoyo a cualquier conferencia o reunión que quiera tener una línea directa del Código de Conducta. Si eres organizador de un evento y quieres utilizar la línea directa del Código de Conducta de Mariatta para tu evento, por favor envíe un correo electrónico a devrel@nexmo.com y estaremos encantados de ayudarle a configurar la aplicación y proporcionarle crédito Nexmo gratuito.

Compartir:

https://a.storyblok.com/f/270183/150x150/a3d03a85fd/placeholder.svg
Mariatta

I am not open, parts of me are broken