Skip to content

Commit

Permalink
Add support for inbound SMS (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
theacodes authored and Mariatta committed Feb 22, 2019
1 parent c89b2e6 commit e360ddf
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 9 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Expand Up @@ -12,7 +12,6 @@ matrix:

before_install:
- python3 -m pip install -U pip
- python3 -m pip install coverage
install:
- python3 -m pip install -U -r dev-requirements.txt
script:
Expand Down
1 change: 1 addition & 0 deletions dev-requirements.txt
Expand Up @@ -2,3 +2,4 @@
pytest==4.2.1
pytest-asyncio==0.10.0
pytest-aiohttp==0.3.0
coverage==4.5.2
6 changes: 5 additions & 1 deletion readme.rst
Expand Up @@ -54,9 +54,13 @@ Deployment

In Heroku, set the environment variables:

- ``NEXMO_API_KEY``: The Nexmo API Key

- ``NEXMO_API_SECRET``: The Nexmo API Key Secret

- ``NEXMO_APP_ID``: The Nexmo App ID

- ``NEXMO_PRIVATE_KEY_VOICE_APP``: The content of the private key (from private.key file).
- ``NEXMO_PRIVATE_KEY_VOICE_APP``: The path to the Nexmo App's private key (private.key file).

It looks like the following:
``----- BEGIN PRIVATE KEY ---- blablahblah ---- END PRIVATE KEY ----``
Expand Down
46 changes: 45 additions & 1 deletion tests/test_webservice.py
@@ -1,7 +1,7 @@
import json
import os
from unittest import mock

import mock
import pytest
from aiohttp import web

Expand All @@ -13,6 +13,10 @@
routes,
)

mock_api_key = "apikey"

mock_api_secret = "sssh"

mock_private_key = """
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCxephXEnphnbrX
Expand All @@ -29,12 +33,16 @@


def test_get_nexmo_client(monkeypatch):
monkeypatch.setitem(os.environ, "NEXMO_API_KEY", mock_api_key)
monkeypatch.setitem(os.environ, "NEXMO_API_SECRET", mock_api_secret)
monkeypatch.setitem(os.environ, "NEXMO_APP_ID", "app_id")
monkeypatch.setitem(os.environ, "NEXMO_PRIVATE_KEY_VOICE_APP", mock_private_key)

client = get_nexmo_client()
assert client.application_id == "app_id"
assert client.private_key == mock_private_key
assert client.api_key == mock_api_key
assert client.api_secret == mock_api_secret


def test_get_phone_numbers(monkeypatch):
Expand Down Expand Up @@ -62,13 +70,17 @@ class FakeNexmoClient:
def __init__(self):
self.calls_created = []
self.speech_sent = []
self.messages_sent = []

def create_call(self, params=None, **kwargs):
self.calls_created.append(params)

def send_speech(self, params=None, **kwargs):
self.speech_sent.append(params)

def send_message(self, params=None, **kwargs):
self.messages_sent.append(params)


@pytest.fixture
def webservice_cli(loop, aiohttp_client, monkeypatch):
Expand Down Expand Up @@ -171,3 +183,35 @@ async def test_answer_call_auto_record(webservice_cli_autorecord):

assert response[1]["record"] is True
assert response[1]["eventUrl"] == ["https://hooks.zapier.com/1111/2222"]


async def test_inbound_sms(webservice_cli):
with mock.patch("webservice.__main__.get_nexmo_client") as mock_nexmo_client:
nexmo_client = FakeNexmoClient()
mock_nexmo_client.return_value = nexmo_client

reporter_number = "1234"
hotline_number = "5678"
text = "onetwothree"

resp = await webservice_cli.get(
f"/webhook/inbound-sms/?msisdn={reporter_number}&to={hotline_number}&text={text}"
)

assert resp.status == 204

# One for each person on staff, one to respond to the reporter.
assert len(nexmo_client.messages_sent) == 3

# Check that the organizers got the message.
for n, phone_number_dict in enumerate(mock_phone_numbers):
message = nexmo_client.messages_sent[n]
assert message["from"] == hotline_number
assert message["to"] == phone_number_dict["phone"]
assert text in message["text"]

# Check the response message
response_message = nexmo_client.messages_sent[-1]
assert response_message["to"] == reporter_number
assert response_message["from"] == hotline_number
assert "CoC" in response_message["text"]
52 changes: 46 additions & 6 deletions webservice/__main__.py
Expand Up @@ -18,10 +18,14 @@

def get_nexmo_client():
"""Return an instance of Nexmo client library"""
api_key = os.environ.get("NEXMO_API_KEY")
api_secret = os.environ.get("NEXMO_API_SECRET")
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)
client = nexmo.Client(
key=api_key, secret=api_secret, application_id=app_id, private_key=private_key
)
return client


Expand Down Expand Up @@ -72,6 +76,7 @@ async def answer_call(request):
Dial everyone on staff, adding them to the same conversation
"""
hotline_number = request.rel_url.query["to"]
conversation_uuid = request.rel_url.query["conversation_uuid"].strip()
call_uuid = request.rel_url.query["uuid"].strip()
greeting = "You've reached the PyCascades Code of Conduct Hotline."
Expand Down Expand Up @@ -105,12 +110,9 @@ async def answer_call(request):
client.create_call(
{
"to": [{"type": "phone", "number": phone_number_dict["phone"]}],
"from": {
"type": "phone",
"number": os.environ.get("NEXMO_HOTLINE_NUMBER"),
},
"from": {"type": "phone", "number": hotline_number},
"answer_url": [
f"https://pycascades-coc-hotline-2019.herokuapp.com/webhook/answer_conference_call/{conversation_uuid}/{call_uuid}/"
f"http://{request.host}/webhook/answer_conference_call/{conversation_uuid}/{call_uuid}/"
],
}
)
Expand Down Expand Up @@ -166,6 +168,44 @@ async def answer_conference_call(request):
return web.json_response(ncco)


@routes.get("/webhook/inbound-sms/")
async def inbound_sms(request):
"""Webhook event that receives and inbound SMS messages and notifies all
staff.
It also sends the sender an acknowledgment.
This should be configured in Nexmo to send using GET.
"""
hotline_number = request.rel_url.query["to"]
from_number = request.rel_url.query["msisdn"]
message = request.rel_url.query["text"]

client = get_nexmo_client()
phone_numbers = get_phone_numbers()

for phone_number_dict in phone_numbers:
client.send_message(
{
# Send from the number the received this message.
"from": hotline_number,
"to": phone_number_dict["phone"],
"text": f"{from_number}: {message}"[:140],
}
)

# Reply to the sender and acknowledge receipt.
client.send_message(
{
"from": hotline_number,
"to": from_number,
"text": "Thanks for contacting the CoC hotline. Someone should follow-up shortly. Note: they may follow up from a different number.",
}
)

return web.Response(status=204)


if __name__ == "__main__": # pragma: no cover
app = web.Application()
app.router.add_routes(routes)
Expand Down

0 comments on commit e360ddf

Please sign in to comment.