Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for inbound SMS #27

Merged
merged 1 commit into from Feb 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.0
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}/"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

],
}
)
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